"""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" # ─── I-WORK-PROACTIVE: Proactive Workstream Feed ───────────────────────────── class TestProactiveFeed: """Test the proactive feed module (I-WORK-PROACTIVE).""" def test_proactive_suggestion_dataclass(self): """ProactiveSuggestion dataclass works correctly.""" from app.ai.proactive_feed import ProactiveSuggestion s = ProactiveSuggestion(trigger="mail.received", title="Test", priority="medium") assert s.trigger == "mail.received" assert s.priority == "medium" d = s.to_dict() assert d["trigger"] == "mail.received" def test_get_cooldown_known_trigger(self): """get_cooldown returns correct cooldown for known triggers.""" from app.ai.proactive_feed import get_cooldown assert get_cooldown("mail.received") == 300 assert get_cooldown("contact.created") == 600 assert get_cooldown("workflow.completed") == 60 def test_get_cooldown_unknown_trigger(self): """get_cooldown returns default for unknown triggers.""" from app.ai.proactive_feed import get_cooldown assert get_cooldown("unknown.trigger") == 300 def test_is_cooled_down_initial(self): """is_cooled_down returns False for first-time trigger.""" from app.ai.proactive_feed import is_cooled_down assert is_cooled_down(uuid.uuid4(), "mail.received") is False def test_mark_suggested_sets_cooldown(self): """mark_suggested sets cooldown for the trigger.""" from app.ai.proactive_feed import is_cooled_down, mark_suggested tid = uuid.uuid4() mark_suggested(tid, "mail.received", "msg-123") assert is_cooled_down(tid, "mail.received", "msg-123") is True def test_filter_by_user_settings_enabled(self): """filter_by_user_settings filters by enabled flag.""" from app.ai.proactive_feed import ProactiveSuggestion, filter_by_user_settings suggestions = [ProactiveSuggestion(trigger="test", priority="medium")] assert len(filter_by_user_settings(suggestions, {"enabled": True})) == 1 assert len(filter_by_user_settings(suggestions, {"enabled": False})) == 0 def test_filter_by_user_settings_min_priority(self): """filter_by_user_settings filters by min_priority.""" from app.ai.proactive_feed import ProactiveSuggestion, filter_by_user_settings suggestions = [ ProactiveSuggestion(trigger="test", priority="low"), ProactiveSuggestion(trigger="test", priority="medium"), ProactiveSuggestion(trigger="test", priority="high"), ] filtered = filter_by_user_settings(suggestions, {"enabled": True, "min_priority": "medium"}) assert len(filtered) == 2 # medium + high @pytest.mark.asyncio async def test_generate_suggestions_mail_received(self): """generate_suggestions creates suggestion for mail.received trigger.""" from app.ai.proactive_feed import generate_suggestions suggestions = await generate_suggestions( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), trigger="mail.received", payload={"entity_id": str(uuid.uuid4()), "sender": "test@example.com"}, ) assert len(suggestions) == 1 assert suggestions[0].trigger == "mail.received" assert suggestions[0].priority == "medium" @pytest.mark.asyncio async def test_generate_suggestions_cooldown_blocks(self): """generate_suggestions returns empty list when in cooldown.""" from app.ai.proactive_feed import generate_suggestions, mark_suggested tid = uuid.uuid4() eid = str(uuid.uuid4()) mark_suggested(tid, "mail.received", eid) suggestions = await generate_suggestions( db=MagicMock(), tenant_id=tid, user_id=uuid.uuid4(), trigger="mail.received", payload={"entity_id": eid}, ) assert len(suggestions) == 0 # ─── I-DASH/I-COST/I-USE: Dashboard & Analytics ────────────────────────────── class TestDashboardAnalytics: """Test the dashboard & analytics module (I-DASH, I-COST, I-USE).""" def test_dashboard_functions_importable(self): """All dashboard functions are importable.""" from app.ai.dashboard import get_platform_dashboard, get_cost_dashboard, get_usage_analytics assert callable(get_platform_dashboard) assert callable(get_cost_dashboard) assert callable(get_usage_analytics) @pytest.mark.asyncio async def test_get_platform_dashboard_returns_dict(self): """get_platform_dashboard returns a dict with expected keys.""" from app.ai.dashboard import get_platform_dashboard # Mock all DB queries to return 0 mock_db = AsyncMock() mock_db.scalar = AsyncMock(return_value=0) mock_db.execute = AsyncMock(return_value=MagicMock(scalars=MagicMock(return_value=[]))) result = await get_platform_dashboard(mock_db, uuid.uuid4()) assert isinstance(result, dict) assert "agents" in result assert "workflows" in result assert "knowledge" in result assert "system_health" in result assert "generated_at" in result @pytest.mark.asyncio async def test_get_cost_dashboard_returns_dict(self): """get_cost_dashboard returns a dict with cost data.""" from app.ai.dashboard import get_cost_dashboard mock_db = AsyncMock() mock_db.scalar = AsyncMock(return_value=0.0) mock_db.execute = AsyncMock(return_value=MagicMock(scalars=MagicMock(return_value=[]))) result = await get_cost_dashboard(mock_db, uuid.uuid4(), days=30) assert isinstance(result, dict) assert "total_cost_usd" in result assert "by_agent" in result assert "period_days" in result assert result["period_days"] == 30 @pytest.mark.asyncio async def test_get_usage_analytics_returns_dict(self): """get_usage_analytics returns a dict with usage data.""" from app.ai.dashboard import get_usage_analytics mock_db = AsyncMock() mock_db.scalar = AsyncMock(return_value=0) result = await get_usage_analytics(mock_db, uuid.uuid4(), days=7) assert isinstance(result, dict) assert "agent_runs" in result assert "workflow_executions" in result assert result["period_days"] == 7 # ─── I-DSGVO/I-DSAR/I-COMP-EXPORT: DSGVO & Compliance ──────────────────────── class TestDSGVOExport: """Test the DSGVO export module (I-DSGVO, I-DSAR, I-COMP-EXPORT).""" def test_dsgvo_functions_importable(self): """All DSGVO functions are importable.""" from app.ai.dsgvo_export import export_user_data, create_dsar_request, export_compliance_evidence assert callable(export_user_data) assert callable(create_dsar_request) assert callable(export_compliance_evidence) @pytest.mark.asyncio async def test_export_user_data_returns_dict(self): """export_user_data returns structured dict with expected sections.""" from app.ai.dsgvo_export import export_user_data mock_db = AsyncMock() mock_db.get = AsyncMock(return_value=None) mock_db.execute = AsyncMock(return_value=MagicMock(scalars=MagicMock(return_value=[]))) result = await export_user_data(mock_db, uuid.uuid4(), uuid.uuid4()) assert isinstance(result, dict) assert "export_metadata" in result assert "core" in result assert "agents" in result assert "audit" in result assert result["export_metadata"]["export_type"] == "dsgvo_data_subject_access" @pytest.mark.asyncio async def test_create_dsar_request_creates_task(self): """create_dsar_request creates a Task with task_type='dsar'.""" from app.ai.dsgvo_export import create_dsar_request with patch("app.plugins.builtins.tasks.services.create_task", new_callable=AsyncMock) as mock_create: mock_create.return_value = {"id": "task-dsar-123", "title": "DSAR: access"} result = await create_dsar_request( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), subject_user_id=uuid.uuid4(), request_type="access", ) assert result["task"]["id"] == "task-dsar-123" assert result["request_type"] == "access" call_args = mock_create.call_args assert call_args[0][3]["task_type"] == "dsar" @pytest.mark.asyncio async def test_export_compliance_evidence_returns_dict(self): """export_compliance_evidence returns structured evidence package.""" from app.ai.dsgvo_export import export_compliance_evidence mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock(scalars=MagicMock(return_value=[]))) result = await export_compliance_evidence(mock_db, uuid.uuid4(), days=90) assert isinstance(result, dict) assert "export_metadata" in result assert "agent_definitions" in result assert "approval_records" in result assert "technical_policies" in result assert result["export_metadata"]["export_type"] == "compliance_evidence" assert result["export_metadata"]["period_days"] == 90 def test_technical_policies_structure(self): """Technical policies have expected structure.""" # This is tested via export_compliance_evidence but we can check the helper from app.ai.dsgvo_export import _get_sensitive_fields fields = _get_sensitive_fields() assert isinstance(fields, dict) assert len(fields) > 0