cleanup: remove all unconnected Phase I+J scaffold code and unconnected Phase F+H modules (workstream_contract, proactive_feed, dashboard, dsgvo_export, onboarding, mcp_exposure, integration_tools, self_improvement, agent_workstream, workflows/workstream, knowledge_sources, knowledge_extraction, knowledge_lifecycle, platform.py routes, 13 frontend files, 2 test files), restore Dashboard.tsx, tsc clean, backend OK
This commit is contained in:
@@ -1,560 +0,0 @@
|
||||
"""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
|
||||
|
||||
|
||||
# ─── I-ONB: Onboarding ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestOnboarding:
|
||||
"""Test the onboarding module (I-ONB)."""
|
||||
|
||||
def test_onboarding_functions_importable(self):
|
||||
"""All onboarding functions are importable."""
|
||||
from app.ai.onboarding import get_onboarding_status, get_onboarding_guide
|
||||
assert callable(get_onboarding_status)
|
||||
assert callable(get_onboarding_guide)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_onboarding_status_returns_dict(self):
|
||||
"""get_onboarding_status returns a dict with steps and progress."""
|
||||
from app.ai.onboarding import get_onboarding_status
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_db.scalar = AsyncMock(return_value=0)
|
||||
|
||||
result = await get_onboarding_status(mock_db, uuid.uuid4(), uuid.uuid4())
|
||||
assert isinstance(result, dict)
|
||||
assert "steps" in result
|
||||
assert "progress_pct" in result
|
||||
assert "welcome" in result["steps"]
|
||||
assert "create_agent" in result["steps"]
|
||||
assert result["steps"]["welcome"]["completed"] is True
|
||||
|
||||
def test_get_onboarding_guide_returns_steps(self):
|
||||
"""get_onboarding_guide returns a list of steps."""
|
||||
from app.ai.onboarding import get_onboarding_guide
|
||||
|
||||
result = get_onboarding_guide()
|
||||
assert isinstance(result, dict)
|
||||
assert "steps" in result
|
||||
assert len(result["steps"]) == 5
|
||||
step_ids = [s["id"] for s in result["steps"]]
|
||||
assert "welcome" in step_ids
|
||||
assert "create_agent" in step_ids
|
||||
assert "create_workflow" in step_ids
|
||||
assert "enable_knowledge" in step_ids
|
||||
assert "enable_workstream" in step_ids
|
||||
@@ -1,270 +0,0 @@
|
||||
"""Tests for Phase J — Controlled Self-Improvement."""
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
|
||||
class TestImprovementSignals:
|
||||
"""Test J-SIGNAL: Improvement signal collection."""
|
||||
|
||||
def test_signal_dataclass(self):
|
||||
from app.ai.self_improvement import ImprovementSignal, SignalType
|
||||
s = ImprovementSignal(signal_type=SignalType.AGENT_RUN, source_ref="agent_run:123", tenant_id="t1")
|
||||
assert s.signal_type == SignalType.AGENT_RUN
|
||||
assert s.source_ref == "agent_run:123"
|
||||
d = s.to_dict()
|
||||
assert d["signal_type"] == "agent_run"
|
||||
|
||||
def test_all_signal_types(self):
|
||||
from app.ai.self_improvement import SignalType
|
||||
assert SignalType.AGENT_RUN.value == "agent_run"
|
||||
assert SignalType.WORKFLOW_RUN.value == "workflow_run"
|
||||
assert SignalType.USER_CORRECTION.value == "user_correction"
|
||||
assert SignalType.HANDOFF.value == "handoff"
|
||||
assert SignalType.ERROR_RETRY.value == "error_retry"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_signals_returns_list(self):
|
||||
from app.ai.self_improvement import collect_signals
|
||||
mock_db = AsyncMock()
|
||||
mock_db.execute = AsyncMock(return_value=MagicMock(scalars=MagicMock(return_value=[])))
|
||||
signals = await collect_signals(mock_db, uuid.uuid4(), days=30)
|
||||
assert isinstance(signals, list)
|
||||
|
||||
|
||||
class TestPatternDetection:
|
||||
"""Test J-PATTERN: Pattern/bottleneck detection."""
|
||||
|
||||
def test_detect_patterns_empty(self):
|
||||
from app.ai.self_improvement import detect_patterns
|
||||
assert detect_patterns([]) == []
|
||||
|
||||
def test_detect_error_retries(self):
|
||||
from app.ai.self_improvement import detect_patterns, ImprovementSignal, SignalType
|
||||
signals = [
|
||||
ImprovementSignal(signal_type=SignalType.AGENT_RUN, outcome="stopped_error"),
|
||||
ImprovementSignal(signal_type=SignalType.AGENT_RUN, outcome="stopped_error"),
|
||||
ImprovementSignal(signal_type=SignalType.AGENT_RUN, outcome="stopped_error"),
|
||||
]
|
||||
patterns = detect_patterns(signals)
|
||||
assert len(patterns) == 1
|
||||
assert patterns[0].pattern_type == "error_retries"
|
||||
assert patterns[0].occurrence_count == 3
|
||||
|
||||
def test_detect_frequent_corrections(self):
|
||||
from app.ai.self_improvement import detect_patterns, ImprovementSignal, SignalType
|
||||
signals = [
|
||||
ImprovementSignal(signal_type=SignalType.USER_CORRECTION, outcome="corrected"),
|
||||
ImprovementSignal(signal_type=SignalType.USER_CORRECTION, outcome="corrected"),
|
||||
ImprovementSignal(signal_type=SignalType.USER_CORRECTION, outcome="corrected"),
|
||||
]
|
||||
patterns = detect_patterns(signals)
|
||||
assert len(patterns) == 1
|
||||
assert patterns[0].pattern_type == "frequent_corrections"
|
||||
|
||||
def test_detect_repetitive_handoffs(self):
|
||||
from app.ai.self_improvement import detect_patterns, ImprovementSignal, SignalType
|
||||
signals = [
|
||||
ImprovementSignal(signal_type=SignalType.HANDOFF, outcome="handoff"),
|
||||
ImprovementSignal(signal_type=SignalType.HANDOFF, outcome="handoff"),
|
||||
ImprovementSignal(signal_type=SignalType.HANDOFF, outcome="handoff"),
|
||||
]
|
||||
patterns = detect_patterns(signals)
|
||||
assert len(patterns) == 1
|
||||
assert patterns[0].pattern_type == "repetitive_handoffs"
|
||||
|
||||
def test_detect_rejected_suggestions(self):
|
||||
from app.ai.self_improvement import detect_patterns, ImprovementSignal, SignalType
|
||||
signals = [
|
||||
ImprovementSignal(signal_type=SignalType.PROACTIVE_SUGGESTION, outcome="dismissed") for _ in range(5)
|
||||
]
|
||||
patterns = detect_patterns(signals)
|
||||
assert len(patterns) == 1
|
||||
assert patterns[0].pattern_type == "rejected_suggestions"
|
||||
|
||||
def test_pattern_confidence_capped(self):
|
||||
from app.ai.self_improvement import detect_patterns, ImprovementSignal, SignalType
|
||||
signals = [ImprovementSignal(signal_type=SignalType.AGENT_RUN, outcome="stopped_error") for _ in range(50)]
|
||||
patterns = detect_patterns(signals)
|
||||
assert patterns[0].confidence <= 0.9
|
||||
|
||||
|
||||
class TestImprovementProposal:
|
||||
"""Test J-PROP: Improvement proposal creation."""
|
||||
|
||||
def test_proposal_dataclass(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus
|
||||
p = ImprovementProposal(proposal_type=ProposalType.AGENT, title="Test")
|
||||
assert p.proposal_type == ProposalType.AGENT
|
||||
assert p.status == ProposalStatus.DRAFT
|
||||
d = p.to_dict()
|
||||
assert d["proposal_type"] == "agent"
|
||||
assert d["status"] == "draft"
|
||||
|
||||
def test_create_proposal_from_pattern(self):
|
||||
from app.ai.self_improvement import DetectedPattern, create_proposal, ProposalType
|
||||
pattern = DetectedPattern(pattern_type="error_retries", description="3 errors", confidence=0.8, occurrence_count=3)
|
||||
proposal = create_proposal(pattern, ProposalType.AGENT, "Fix agent errors")
|
||||
assert proposal.title == "Fix agent errors"
|
||||
assert proposal.proposal_type == ProposalType.AGENT
|
||||
assert len(proposal.evidence_refs) == 0 # pattern had no refs
|
||||
assert proposal.status.value == "draft"
|
||||
|
||||
def test_all_proposal_types(self):
|
||||
from app.ai.self_improvement import ProposalType
|
||||
assert ProposalType.AGENT.value == "agent"
|
||||
assert ProposalType.SKILL.value == "skill"
|
||||
assert ProposalType.WORKFLOW.value == "workflow"
|
||||
assert ProposalType.PLUGIN_PATCH.value == "plugin_patch"
|
||||
|
||||
def test_all_proposal_statuses(self):
|
||||
from app.ai.self_improvement import ProposalStatus
|
||||
assert ProposalStatus.DRAFT.value == "draft"
|
||||
assert ProposalStatus.PENDING_APPROVAL.value == "pending_approval"
|
||||
assert ProposalStatus.APPROVED.value == "approved"
|
||||
assert ProposalStatus.ACTIVE.value == "active"
|
||||
assert ProposalStatus.ROLLED_BACK.value == "rolled_back"
|
||||
|
||||
|
||||
class TestVersionedDraft:
|
||||
"""Test J-DRAFT: Versioned draft creation."""
|
||||
|
||||
def test_draft_dataclass(self):
|
||||
from app.ai.self_improvement import VersionedDraft
|
||||
d = VersionedDraft(proposal_id="p1", version=1, config={"key": "value"})
|
||||
assert d.version == 1
|
||||
assert d.config == {"key": "value"}
|
||||
assert d.previous_version_id is None
|
||||
|
||||
def test_create_draft_first_version(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, create_draft
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT, draft_config={"model": "gpt-4o"})
|
||||
draft = create_draft(proposal)
|
||||
assert draft.version == 1
|
||||
assert draft.config == {"model": "gpt-4o"}
|
||||
assert draft.previous_version_id is None
|
||||
|
||||
def test_create_draft_incremented_version(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, create_draft, VersionedDraft
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT, draft_config={"model": "gpt-4o-mini"})
|
||||
prev = VersionedDraft(proposal_id="p1", version=1, config={"model": "gpt-4o"})
|
||||
draft = create_draft(proposal, prev)
|
||||
assert draft.version == 2
|
||||
assert draft.previous_version_id == prev.id
|
||||
|
||||
|
||||
class TestEvaluation:
|
||||
"""Test J-EVAL: Evaluation/sandbox."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_proposal_returns_result(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, VersionedDraft, evaluate_proposal, ImprovementSignal, SignalType
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT)
|
||||
draft = VersionedDraft(proposal_id=proposal.id, version=1)
|
||||
signals = [ImprovementSignal(signal_type=SignalType.AGENT_RUN, outcome="stopped_error") for _ in range(10)]
|
||||
result = await evaluate_proposal(proposal, draft, signals)
|
||||
assert result["test_cases"] == 10
|
||||
assert result["passed"] == 10
|
||||
assert result["score"] == 100.0
|
||||
assert result["recommendation"] == "approve"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_empty_signals(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, VersionedDraft, evaluate_proposal
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT)
|
||||
draft = VersionedDraft(proposal_id=proposal.id, version=1)
|
||||
result = await evaluate_proposal(proposal, draft, [])
|
||||
assert result["test_cases"] == 0
|
||||
assert result["score"] == 0.0
|
||||
|
||||
|
||||
class TestApprovalActivation:
|
||||
"""Test J-APPROVAL, J-ACTIVATE: Approval and activation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_approval(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus, request_approval
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT)
|
||||
with patch("app.core.approval.create_approval_request", new_callable=AsyncMock) as mock_approval:
|
||||
mock_approval.return_value = MagicMock(id=uuid.uuid4())
|
||||
result = await request_approval(AsyncMock(), uuid.uuid4(), uuid.uuid4(), proposal, {"score": 80})
|
||||
assert result["status"] == "pending_approval"
|
||||
assert proposal.status == ProposalStatus.PENDING_APPROVAL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activate_without_approval_rejected(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus, VersionedDraft, activate_proposal
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT, status=ProposalStatus.DRAFT)
|
||||
draft = VersionedDraft(proposal_id=proposal.id, version=1)
|
||||
result = await activate_proposal(AsyncMock(), uuid.uuid4(), proposal, draft)
|
||||
assert result["status"] == "rejected"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activate_approved_proposal(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus, VersionedDraft, activate_proposal
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT, status=ProposalStatus.APPROVED)
|
||||
draft = VersionedDraft(proposal_id=proposal.id, version=1)
|
||||
result = await activate_proposal(AsyncMock(), uuid.uuid4(), proposal, draft)
|
||||
assert result["status"] == "active"
|
||||
assert result["rollback_available"] is True
|
||||
assert proposal.status == ProposalStatus.ACTIVE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rollback_active_proposal(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus, VersionedDraft, rollback_proposal
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT, status=ProposalStatus.ACTIVE)
|
||||
prev_draft = VersionedDraft(proposal_id=proposal.id, version=1)
|
||||
result = await rollback_proposal(AsyncMock(), uuid.uuid4(), proposal, prev_draft)
|
||||
assert result["status"] == "rolled_back"
|
||||
assert proposal.status == ProposalStatus.ROLLED_BACK
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rollback_non_active_rejected(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus, rollback_proposal
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT, status=ProposalStatus.DRAFT)
|
||||
result = await rollback_proposal(AsyncMock(), uuid.uuid4(), proposal)
|
||||
assert result["status"] == "rejected"
|
||||
|
||||
|
||||
class TestImpactMeasurement:
|
||||
"""Test J-MEASURE: Pre/post impact measurement."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_measure_impact_not_active(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, measure_impact
|
||||
proposal = ImprovementProposal(proposal_type=ProposalType.AGENT)
|
||||
result = await measure_impact(AsyncMock(), uuid.uuid4(), proposal)
|
||||
assert result["status"] == "not_active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_measure_impact_active(self):
|
||||
from app.ai.self_improvement import ImprovementProposal, ProposalType, ProposalStatus, measure_impact
|
||||
proposal = ImprovementProposal(
|
||||
proposal_type=ProposalType.AGENT,
|
||||
status=ProposalStatus.ACTIVE,
|
||||
activated_at=datetime.now(UTC) - timedelta(days=3),
|
||||
measurement_before={"total_runs": 10, "errors": 5, "cost_usd": 1.0, "error_rate": 50.0},
|
||||
)
|
||||
mock_db = MagicMock()
|
||||
mock_scalar = AsyncMock(side_effect=[20, 2, 0.5])
|
||||
mock_db.scalar = mock_scalar
|
||||
result = await measure_impact(mock_db, uuid.uuid4(), proposal, days=7)
|
||||
assert isinstance(result, dict)
|
||||
assert "proposal_id" in result
|
||||
assert "period_days" in result
|
||||
assert result["period_days"] == 7
|
||||
# After may contain error if mock DB queries fail, or metrics if they succeed
|
||||
assert "after" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_baseline(self):
|
||||
from app.ai.self_improvement import capture_baseline
|
||||
mock_db = MagicMock()
|
||||
mock_scalar = AsyncMock(side_effect=[10, 2, 0.5])
|
||||
mock_db.scalar = mock_scalar
|
||||
result = await capture_baseline(mock_db, uuid.uuid4(), days=7)
|
||||
assert isinstance(result, dict)
|
||||
# Result may contain metrics or error depending on mock DB behavior
|
||||
assert len(result) > 0
|
||||
Reference in New Issue
Block a user