fix(i-e): BUG-099 geschlossen — tote workstream-Tests entfernt, Import-Test korrigiert
app.ai.agent_workstream und app.workflows.workstream sind geloescht (Phase-2-Roadmap); lazy Imports brachen zur Laufzeit. Chirurgische Entfernung: TestWorkstream-Klasse phase_f (120 Zeilen), TestWorkflowWorkstream + G-WORK-Sektion phase_g (73 Zeilen), test_workstream_to_task_transition spike_i. test_all_modules_importable auf existierende Exporte korrigiert (fetch_source_content->get_available_sources, auto_create_relationships->filter_high_confidence); alle anderen Module via importlib-Check verifiziert OK. ruff: 21 Findings auto-gefixt, 1 F841-Vorbestand belassen. Beweis: phase_f+phase_g+spike_i 88/88 passed in 19.16s; to_workstream_block()-Tests bleiben valide (existiert in app.ai.knowledge_sources).
This commit is contained in:
@@ -17,17 +17,15 @@ Covers:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import UTC, datetime
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
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 (
|
from app.ai.agent_permissions import (
|
||||||
AgentPermissionContext,
|
AgentPermissionContext,
|
||||||
check_agent_execute_permission,
|
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.context_builder import ReActSystemPromptBuilder, build_agent_context
|
||||||
from app.ai.data_policy import enforce_data_policy
|
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.ai.transparency import is_ai_participant, mark_as_ai_generated
|
||||||
from app.core.approval import (
|
from app.core.approval import (
|
||||||
ApprovalRequest,
|
ApprovalRequest,
|
||||||
@@ -47,7 +45,6 @@ from app.core.approval import (
|
|||||||
)
|
)
|
||||||
from app.core.error_codes import ApiError
|
from app.core.error_codes import ApiError
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
# No-op overrides of conftest DB fixtures — these tests use mocks only
|
# 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
|
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
|
# 9. Budget limit tests
|
||||||
# ══════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ All tests use mocks — no real DB/LLM/Redis/HTTP needed.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
@@ -14,12 +13,11 @@ import pytest
|
|||||||
|
|
||||||
from app.workflows.step_handlers import (
|
from app.workflows.step_handlers import (
|
||||||
StepResult,
|
StepResult,
|
||||||
get_step_handler,
|
|
||||||
get_available_step_types,
|
|
||||||
_is_url_safe,
|
_is_url_safe,
|
||||||
|
get_available_step_types,
|
||||||
|
get_step_handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ─── Step Handler Registry ────────────────────────────────────────────────────
|
# ─── Step Handler Registry ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -308,7 +306,6 @@ class TestWorkflowEngineResume:
|
|||||||
async def test_find_resumable_workflows_query(self):
|
async def test_find_resumable_workflows_query(self):
|
||||||
"""find_resumable_workflows queries for waiting instances with passed resume_at."""
|
"""find_resumable_workflows queries for waiting instances with passed resume_at."""
|
||||||
from app.workflows.engine import find_resumable_workflows
|
from app.workflows.engine import find_resumable_workflows
|
||||||
from app.models.workflow import WorkflowInstance
|
|
||||||
|
|
||||||
db = MagicMock()
|
db = MagicMock()
|
||||||
mock_result = MagicMock()
|
mock_result = MagicMock()
|
||||||
@@ -335,9 +332,10 @@ class TestWorkflowSchema:
|
|||||||
|
|
||||||
def test_step_schema_rejects_unknown_type(self):
|
def test_step_schema_rejects_unknown_type(self):
|
||||||
"""WorkflowStep schema rejects unknown step types."""
|
"""WorkflowStep schema rejects unknown step types."""
|
||||||
from app.schemas.workflow import WorkflowStep
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from app.schemas.workflow import WorkflowStep
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
WorkflowStep(name="Bad", type="unknown_type", config={})
|
WorkflowStep(name="Bad", type="unknown_type", config={})
|
||||||
|
|
||||||
@@ -464,75 +462,3 @@ class TestDecisionGuard:
|
|||||||
assert "send_email" in result["reason"]
|
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
|
|
||||||
|
|||||||
@@ -31,34 +31,27 @@ class TestSpikeIIntegrationFlow:
|
|||||||
# Agent
|
# Agent
|
||||||
from app.ai.agent_loop import run_react_loop
|
from app.ai.agent_loop import run_react_loop
|
||||||
from app.ai.agent_permissions import resolve_agent_permissions
|
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
|
# Search
|
||||||
from app.plugins.builtins.unified_search.contracts import UnifiedSearchContract
|
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
|
# Workflow
|
||||||
from app.workflows.engine import WorkflowEngine
|
from app.workflows.engine import WorkflowEngine
|
||||||
from app.workflows.step_handlers import get_step_handler
|
|
||||||
|
|
||||||
# All imports successful — no circular dependencies
|
# All imports successful — no circular dependencies
|
||||||
assert run_react_loop is not None
|
assert run_react_loop is not None
|
||||||
assert resolve_agent_permissions is not None
|
assert resolve_agent_permissions is not None
|
||||||
assert UnifiedSearchContract 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_task is not None
|
||||||
assert create_approval_request is not None
|
assert create_approval_request is not None
|
||||||
assert WorkflowEngine is not None
|
assert WorkflowEngine is not None
|
||||||
@@ -132,28 +125,6 @@ class TestSpikeIIntegrationFlow:
|
|||||||
# This block can be posted via Communication contract
|
# This block can be posted via Communication contract
|
||||||
# (In production, post_workflow_status or post_agent_message would be used)
|
# (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):
|
async def test_task_to_approval_transition(self):
|
||||||
"""Task can require approval — ApprovalRequest is created."""
|
"""Task can require approval — ApprovalRequest is created."""
|
||||||
from app.core.approval import create_approval_request
|
from app.core.approval import create_approval_request
|
||||||
|
|||||||
Reference in New Issue
Block a user