"""Tests for Phase I — Integration tools: I-AW, I-AK.""" from __future__ import annotations import uuid from unittest.mock import AsyncMock, MagicMock, patch import pytest class TestIntegrationTools: """Test the integration tools module (I-AW, I-AK).""" def test_all_tools_importable(self): """All integration tools are importable.""" from app.ai.integration_tools import ( start_workflow_tool, check_workflow_status_tool, ask_knowledge_tool, search_knowledge_tool, register_integration_tools, ) assert callable(start_workflow_tool) assert callable(check_workflow_status_tool) assert callable(ask_knowledge_tool) assert callable(search_knowledge_tool) assert callable(register_integration_tools) @pytest.mark.asyncio async def test_start_workflow_tool_returns_result(self): """start_workflow_tool returns instance info on success.""" from app.ai.integration_tools import start_workflow_tool with patch("app.services.workflow_service.create_instance", new_callable=AsyncMock) as mock_create: mock_create.return_value = {"id": "inst-123", "status": "pending"} result = await start_workflow_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), workflow_id="wf-123", context={"key": "value"}, ) assert result["instance_id"] == "inst-123" assert result["status"] == "pending" assert result["workflow_id"] == "wf-123" @pytest.mark.asyncio async def test_start_workflow_tool_handles_not_found(self): """start_workflow_tool returns error when workflow not found.""" from app.ai.integration_tools import start_workflow_tool with patch("app.services.workflow_service.create_instance", new_callable=AsyncMock) as mock_create: mock_create.return_value = None result = await start_workflow_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), workflow_id="nonexistent", ) assert result["error"] == "Workflow not found" assert result["status"] == "not_found" @pytest.mark.asyncio async def test_ask_knowledge_tool_delegates_to_ask_knowledge(self): """ask_knowledge_tool delegates to knowledge_lifecycle.ask_knowledge.""" from app.ai.integration_tools import ask_knowledge_tool with patch("app.ai.knowledge_lifecycle.ask_knowledge", new_callable=AsyncMock) as mock_ask: mock_ask.return_value = {"answer": "Test answer", "evidence": [], "query": "test"} result = await ask_knowledge_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), query="test query", ) assert result["answer"] == "Test answer" mock_ask.assert_called_once() @pytest.mark.asyncio async def test_search_knowledge_tool_handles_no_search(self): """search_knowledge_tool returns error when search not available.""" from app.ai.integration_tools import search_knowledge_tool with patch("app.plugins.builtins.unified_search.contracts.UnifiedSearchContract") as mock_contract: mock_contract.get_function = MagicMock(return_value=None) result = await search_knowledge_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), query="test", ) assert result["error"] == "Search not available" assert result["results"] == [] @pytest.mark.asyncio async def test_check_workflow_status_tool_handles_not_found(self): """check_workflow_status_tool returns error when instance not found.""" from app.ai.integration_tools import check_workflow_status_tool with patch("app.services.workflow_service.get_instance", new_callable=AsyncMock) as mock_get: mock_get.return_value = None result = await check_workflow_status_tool( db=MagicMock(), tenant_id=uuid.uuid4(), instance_id=str(uuid.uuid4()), ) assert result["error"] == "Instance not found" assert result["status"] == "not_found" # ─── I-APPR-LOOP: Agent Loop Human-in-the-Loop Approval ────────────────────── class TestAgentLoopApproval: """Test the I-APPR-LOOP approval integration in run_react_loop.""" def test_run_react_loop_has_approval_params(self): """run_react_loop has require_approval and approval_tools parameters.""" import inspect from app.ai.agent_loop import run_react_loop sig = inspect.signature(run_react_loop) assert "require_approval" in sig.parameters assert "approval_tools" in sig.parameters assert sig.parameters["require_approval"].default is False assert sig.parameters["approval_tools"].default is None def test_react_result_has_waiting_for_approval_status(self): """ReActResult supports waiting_for_approval status.""" from app.ai.agent_loop import ReActResult result = ReActResult(final_content="", status="waiting_for_approval") assert result.status == "waiting_for_approval" assert result.final_content == "" # ─── I-MCP: MCP-Exposure ───────────────────────────────────────────────────── class TestMCPExposure: """Test the MCP exposure layer (I-MCP).""" def test_mcp_tools_count(self): """6 MCP tools are defined.""" from app.ai.mcp_exposure import MCP_TOOLS assert len(MCP_TOOLS) == 6 def test_mcp_tool_names(self): """MCP tools have correct names.""" from app.ai.mcp_exposure import MCP_TOOLS names = {t["name"] for t in MCP_TOOLS} assert names == {"search", "ask_knowledge", "start_workflow", "check_workflow_status", "list_agents", "create_task"} def test_get_mcp_tools_returns_schemas(self): """get_mcp_tools returns tool schemas without internal fields.""" from app.ai.mcp_exposure import get_mcp_tools tools = get_mcp_tools() for t in tools: assert "name" in t assert "description" in t assert "input_schema" in t assert "required_permission" not in t # Internal field not exposed assert "handler" not in t # Internal field not exposed def test_get_mcp_tool_existing(self): """get_mcp_tool returns tool definition for existing tool.""" from app.ai.mcp_exposure import get_mcp_tool tool = get_mcp_tool("search") assert tool is not None assert tool["name"] == "search" assert tool["required_permission"] == "contacts:read" def test_get_mcp_tool_nonexistent(self): """get_mcp_tool returns None for unknown tool.""" from app.ai.mcp_exposure import get_mcp_tool assert get_mcp_tool("nonexistent") is None @pytest.mark.asyncio async def test_execute_mcp_tool_unknown(self): """execute_mcp_tool returns error for unknown tool.""" from app.ai.mcp_exposure import execute_mcp_tool result = await execute_mcp_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), tool_name="nonexistent", arguments={}, ) assert result["status"] == "not_found" @pytest.mark.asyncio async def test_execute_mcp_tool_permission_denied(self): """execute_mcp_tool returns forbidden when permission missing.""" from app.ai.mcp_exposure import execute_mcp_tool result = await execute_mcp_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), tool_name="start_workflow", arguments={"workflow_id": "test"}, user_permissions={"permissions": [], "denied_permissions": [], "is_system_admin": False}, ) assert result["status"] == "forbidden" @pytest.mark.asyncio async def test_execute_mcp_tool_search(self): """execute_mcp_tool delegates to search_knowledge_tool for 'search'.""" from app.ai.mcp_exposure import execute_mcp_tool with patch("app.ai.integration_tools.search_knowledge_tool", new_callable=AsyncMock) as mock_search: mock_search.return_value = {"results": [], "total": 0, "query": "test"} result = await execute_mcp_tool( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), tool_name="search", arguments={"query": "test"}, user_permissions={"is_system_admin": True}, ) assert result["query"] == "test" mock_search.assert_called_once()