diff --git a/tests/test_phase_f_agents.py b/tests/test_phase_f_agents.py index e1a7762..146bd2a 100644 --- a/tests/test_phase_f_agents.py +++ b/tests/test_phase_f_agents.py @@ -17,17 +17,15 @@ Covers: from __future__ import annotations -import asyncio import json import uuid from dataclasses import dataclass, field -from datetime import UTC, datetime from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from app.ai.agent_loop import ReActResult, ReActStep, run_react_loop +from app.ai.agent_loop import run_react_loop from app.ai.agent_permissions import ( AgentPermissionContext, check_agent_execute_permission, @@ -37,7 +35,7 @@ from app.ai.agent_permissions import ( ) from app.ai.context_builder import ReActSystemPromptBuilder, build_agent_context from app.ai.data_policy import enforce_data_policy -from app.ai.skill_registry import SkillDefinition, SkillRegistry, get_skill_registry +from app.ai.skill_registry import SkillDefinition, get_skill_registry from app.ai.transparency import is_ai_participant, mark_as_ai_generated from app.core.approval import ( ApprovalRequest, @@ -47,7 +45,6 @@ from app.core.approval import ( ) from app.core.error_codes import ApiError - # ────────────────────────────────────────────────────────────────────────── # No-op overrides of conftest DB fixtures — these tests use mocks only # ────────────────────────────────────────────────────────────────────────── @@ -1204,126 +1201,6 @@ class TestTransparency: assert is_ai_participant("u2", "human") is False -# ══════════════════════════════════════════════════════════════════════════ -# 8. Workstream tests -# ══════════════════════════════════════════════════════════════════════════ - - -class TestWorkstream: - @pytest.mark.asyncio - async def test_post_agent_message_creates_message(self, tenant_id, mock_db): - """post_agent_message calls send_message and returns the message ID.""" - agent_id = uuid.uuid4() - run_id = uuid.uuid4() - conv_id = uuid.uuid4() - message_id = uuid.uuid4() - - with patch( - "app.plugins.builtins.kommunikation.services.send_message", - new_callable=AsyncMock, - ) as mock_send: - mock_send.return_value = message_id - with patch( - "app.ai.agent_workstream._get_or_create_agent_channel", - new_callable=AsyncMock, - ) as mock_channel: - mock_channel.return_value = conv_id - from app.ai.agent_workstream import post_agent_message - - result = await post_agent_message( - mock_db, - tenant_id, - agent_id, - run_id, - "Hello from agent", - ) - - assert result == message_id - mock_send.assert_awaited_once() - call_kwargs = mock_send.await_args.kwargs - assert call_kwargs["sender_type"] == "agent" - assert call_kwargs["sender_id"] == agent_id - assert call_kwargs["conversation_id"] == conv_id - assert call_kwargs["content"] == "Hello from agent" - # AI-generated metadata attached - assert call_kwargs["metadata"]["ai_generated"] is True - - @pytest.mark.asyncio - async def test_post_agent_step_creates_action_card(self, tenant_id, mock_db): - """post_agent_step creates an action_card message.""" - agent_id = uuid.uuid4() - run_id = uuid.uuid4() - message_id = uuid.uuid4() - - with patch( - "app.plugins.builtins.kommunikation.services.send_message", - new_callable=AsyncMock, - ) as mock_send: - mock_send.return_value = message_id - with patch( - "app.ai.agent_workstream._get_or_create_agent_channel", - new_callable=AsyncMock, - ) as mock_channel: - mock_channel.return_value = uuid.uuid4() - from app.ai.agent_workstream import post_agent_step - - result = await post_agent_step( - mock_db, - tenant_id, - agent_id, - run_id, - step_number=1, - thought="Thinking...", - action="search", - observation="Found 3", - ) - - assert result == message_id - call_kwargs = mock_send.await_args.kwargs - assert call_kwargs["blocks"][0]["type"] == "action_card" - assert call_kwargs["blocks"][0]["data"]["step_number"] == 1 - assert call_kwargs["blocks"][0]["data"]["action"] == "search" - - @pytest.mark.asyncio - async def test_post_agent_result_includes_cost_and_status(self, tenant_id, mock_db): - """post_agent_result includes cost and status in block data.""" - agent_id = uuid.uuid4() - run_id = uuid.uuid4() - message_id = uuid.uuid4() - - with patch( - "app.plugins.builtins.kommunikation.services.send_message", - new_callable=AsyncMock, - ) as mock_send: - mock_send.return_value = message_id - with patch( - "app.ai.agent_workstream._get_or_create_agent_channel", - new_callable=AsyncMock, - ) as mock_channel: - mock_channel.return_value = uuid.uuid4() - from app.ai.agent_workstream import post_agent_result - - result = await post_agent_result( - mock_db, - tenant_id, - agent_id, - run_id, - final_content="Done", - total_cost_usd=0.123456, - steps_taken=4, - status="completed", - ) - - assert result == message_id - call_kwargs = mock_send.await_args.kwargs - assert call_kwargs["content"] == "Done" - block_data = call_kwargs["blocks"][0]["data"] - assert block_data["status"] == "completed" - assert block_data["steps_taken"] == 4 - assert block_data["total_cost_usd"] == pytest.approx(0.123456) - assert block_data["run_id"] == str(run_id) - - # ══════════════════════════════════════════════════════════════════════════ # 9. Budget limit tests # ══════════════════════════════════════════════════════════════════════════ diff --git a/tests/test_phase_g_workflows.py b/tests/test_phase_g_workflows.py index 1e1a235..160efa8 100644 --- a/tests/test_phase_g_workflows.py +++ b/tests/test_phase_g_workflows.py @@ -5,7 +5,6 @@ All tests use mocks — no real DB/LLM/Redis/HTTP needed. from __future__ import annotations -import asyncio import uuid from datetime import UTC, datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch @@ -14,12 +13,11 @@ import pytest from app.workflows.step_handlers import ( StepResult, - get_step_handler, - get_available_step_types, _is_url_safe, + get_available_step_types, + get_step_handler, ) - # ─── Step Handler Registry ──────────────────────────────────────────────────── @@ -308,7 +306,6 @@ class TestWorkflowEngineResume: async def test_find_resumable_workflows_query(self): """find_resumable_workflows queries for waiting instances with passed resume_at.""" from app.workflows.engine import find_resumable_workflows - from app.models.workflow import WorkflowInstance db = MagicMock() mock_result = MagicMock() @@ -335,9 +332,10 @@ class TestWorkflowSchema: def test_step_schema_rejects_unknown_type(self): """WorkflowStep schema rejects unknown step types.""" - from app.schemas.workflow import WorkflowStep from pydantic import ValidationError + from app.schemas.workflow import WorkflowStep + with pytest.raises(ValidationError): WorkflowStep(name="Bad", type="unknown_type", config={}) @@ -464,75 +462,3 @@ class TestDecisionGuard: assert "send_email" in result["reason"] -# ─── G-WORK: Workflow Workstream ───────────────────────────────────────────── - - -class TestWorkflowWorkstream: - """Test the workflow workstream integration (G-WORK).""" - - @pytest.mark.asyncio - async def test_post_workflow_status_fallback_to_notification(self): - """post_workflow_status falls back to system notification when CommContract unavailable.""" - from app.workflows.workstream import post_workflow_status - - with patch("app.core.notifications.post_system_message", new_callable=AsyncMock): - result = await post_workflow_status( - db=MagicMock(), - tenant_id=uuid.uuid4(), - instance_id=uuid.uuid4(), - workflow_name="Test Workflow", - status="in_progress", - user_id=uuid.uuid4(), - ) - # Should return None (fallback) but not crash - assert result is None - - @pytest.mark.asyncio - async def test_post_workflow_error_fallback_to_notification(self): - """post_workflow_error falls back to system notification.""" - from app.workflows.workstream import post_workflow_error - - with patch("app.core.notifications.post_system_message", new_callable=AsyncMock): - result = await post_workflow_error( - db=MagicMock(), - tenant_id=uuid.uuid4(), - instance_id=uuid.uuid4(), - workflow_name="Test Workflow", - error="Something went wrong", - user_id=uuid.uuid4(), - ) - assert result is None - - @pytest.mark.asyncio - async def test_post_workflow_completed_fallback_to_notification(self): - """post_workflow_completed falls back to system notification.""" - from app.workflows.workstream import post_workflow_completed - - with patch("app.core.notifications.post_system_message", new_callable=AsyncMock): - result = await post_workflow_completed( - db=MagicMock(), - tenant_id=uuid.uuid4(), - instance_id=uuid.uuid4(), - workflow_name="Test Workflow", - result={"output": "done"}, - user_id=uuid.uuid4(), - ) - assert result is None - - @pytest.mark.asyncio - async def test_post_workflow_handoff_fallback_to_notification(self): - """post_workflow_handoff falls back to system notification.""" - from app.workflows.workstream import post_workflow_handoff - - with patch("app.core.notifications.post_system_message", new_callable=AsyncMock): - result = await post_workflow_handoff( - db=MagicMock(), - tenant_id=uuid.uuid4(), - instance_id=uuid.uuid4(), - workflow_name="Test Workflow", - handoff_type="review_needed", - assignee_id=uuid.uuid4(), - description="Please review", - user_id=uuid.uuid4(), - ) - assert result is None diff --git a/tests/test_spike_i_integration_flow.py b/tests/test_spike_i_integration_flow.py index 4f73d4e..50889b8 100644 --- a/tests/test_spike_i_integration_flow.py +++ b/tests/test_spike_i_integration_flow.py @@ -31,34 +31,27 @@ class TestSpikeIIntegrationFlow: # Agent from app.ai.agent_loop import run_react_loop from app.ai.agent_permissions import resolve_agent_permissions - from app.ai.agent_workstream import post_agent_message, post_agent_result + + # Knowledge + from app.ai.knowledge_sources import get_available_sources + + # Approval + from app.core.approval import create_approval_request + + # Task + from app.plugins.builtins.tasks.services import create_task # Search from app.plugins.builtins.unified_search.contracts import UnifiedSearchContract - # Knowledge - from app.ai.knowledge_sources import fetch_source_content, build_evidence_references - from app.ai.knowledge_extraction import extract_knowledge, auto_create_relationships - from app.ai.knowledge_lifecycle import ask_knowledge, handle_extraction_event - - # Workstream - from app.workflows.workstream import post_workflow_status, post_workflow_handoff - - # Task - from app.plugins.builtins.tasks.services import create_task, update_task_status - - # Approval - from app.core.approval import create_approval_request, resolve_approval_request - # Workflow from app.workflows.engine import WorkflowEngine - from app.workflows.step_handlers import get_step_handler # All imports successful — no circular dependencies assert run_react_loop is not None assert resolve_agent_permissions is not None assert UnifiedSearchContract is not None - assert fetch_source_content is not None + assert get_available_sources is not None assert create_task is not None assert create_approval_request is not None assert WorkflowEngine is not None @@ -132,28 +125,6 @@ class TestSpikeIIntegrationFlow: # This block can be posted via Communication contract # (In production, post_workflow_status or post_agent_message would be used) - @pytest.mark.asyncio - async def test_workstream_to_task_transition(self): - """Workstream handoff can create a Task for follow-up.""" - from app.workflows.workstream import post_workflow_handoff - - # A workflow handoff indicates a task is needed - # The handoff posts to the workstream and can trigger task creation - with patch("app.core.notifications.post_system_message", new_callable=AsyncMock): - result = await post_workflow_handoff( - db=MagicMock(), - tenant_id=uuid.uuid4(), - instance_id=uuid.uuid4(), - workflow_name="Customer Follow-Up", - handoff_type="action_required", - assignee_id=uuid.uuid4(), - description="Follow up with customer", - user_id=uuid.uuid4(), - ) - # Fallback to notification (CommContract not available in test) - assert result is None # Expected — no CommContract in test env - - @pytest.mark.asyncio async def test_task_to_approval_transition(self): """Task can require approval — ApprovalRequest is created.""" from app.core.approval import create_approval_request