"""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() # ─── I-WORK-BASE/ACTOR/HANDOFF: Workstream Contract ────────────────────────── class TestWorkstreamContract: """Test the workstream contract module (I-WORK-BASE, I-WORK-ACTOR, I-WORK-HANDOFF).""" def test_workstream_block_dataclass(self): """WorkstreamBlock dataclass works correctly.""" from app.ai.workstream_contract import WorkstreamBlock block = WorkstreamBlock(type="text", content="Hello", metadata={"key": "value"}) assert block.type == "text" assert block.content == "Hello" d = block.to_dict() assert d["type"] == "text" assert d["content"] == "Hello" def test_workstream_message_dataclass(self): """WorkstreamMessage dataclass works correctly.""" from app.ai.workstream_contract import WorkstreamMessage, WorkstreamBlock msg = WorkstreamMessage(actor_type="agent", actor_id="abc-123", content="Test", blocks=[WorkstreamBlock(type="text")]) assert msg.actor_type == "agent" assert msg.actor_id == "abc-123" d = msg.to_dict() assert d["actor_type"] == "agent" assert len(d["blocks"]) == 1 def test_build_entity_card(self): """build_entity_card creates correct block.""" from app.ai.workstream_contract import build_entity_card block = build_entity_card("contact", "123", "John Doe", "CEO", "/contacts/123") assert block.type == "entity_card" assert block.metadata["entity_type"] == "contact" assert block.metadata["title"] == "John Doe" def test_build_action_card(self): """build_action_card creates correct block.""" from app.ai.workstream_contract import build_action_card block = build_action_card("Approve?", [{"label": "Yes", "action": "approve"}], "Please approve") assert block.type == "action_card" assert len(block.metadata["actions"]) == 1 def test_build_evidence_card(self): """build_evidence_card creates correct block.""" from app.ai.workstream_contract import build_evidence_card block = build_evidence_card("wiki", "456", "Article", "Snippet", "/wiki/456", 0.9) assert block.type == "evidence_card" assert block.metadata["confidence"] == 0.9 def test_build_approval_card(self): """build_approval_card creates correct block.""" from app.ai.workstream_contract import build_approval_card block = build_approval_card("appr-123", "send_email", "Please approve") assert block.type == "approval_card" assert block.metadata["approval_id"] == "appr-123" def test_build_miniapp_block(self): """build_miniapp_block creates correct block.""" from app.ai.workstream_contract import build_miniapp_block block = build_miniapp_block("calendar-app", "Calendar", {"type": "form"}) assert block.type == "miniapp" assert block.metadata["app_id"] == "calendar-app" @pytest.mark.asyncio async def test_post_to_workstream_fallback(self): """post_to_workstream falls back to notification when CommContract unavailable.""" from app.ai.workstream_contract import post_to_workstream, WorkstreamMessage, WorkstreamBlock with patch("app.core.notifications.post_system_message", new_callable=AsyncMock): result = await post_to_workstream( db=MagicMock(), tenant_id=uuid.uuid4(), message=WorkstreamMessage(actor_type="human", actor_id=str(uuid.uuid4()), content="Test", blocks=[WorkstreamBlock(type="text")]), ) assert result is None # Fallback @pytest.mark.asyncio async def test_create_handoff_creates_task(self): """create_handoff creates a Task with task_type='handoff'.""" from app.ai.workstream_contract import create_handoff with patch("app.plugins.builtins.tasks.services.create_task", new_callable=AsyncMock) as mock_create: mock_create.return_value = {"id": "task-123", "title": "Handoff: review_needed"} with patch("app.ai.workstream_contract.post_to_workstream", new_callable=AsyncMock): result = await create_handoff( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), handoff_type="review_needed", description="Please review", ) assert result["task"]["id"] == "task-123" mock_create.assert_called_once() # Verify task_type is 'handoff' call_args = mock_create.call_args assert call_args[0][3]["task_type"] == "handoff"