chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_event import (
|
||||
KernelProcessEvent,
|
||||
KernelProcessEventVisibility,
|
||||
)
|
||||
from semantic_kernel.processes.local_runtime.local_event import LocalEvent
|
||||
|
||||
|
||||
def test_initialization_with_namespace():
|
||||
# Arrange
|
||||
namespace = "test_namespace"
|
||||
inner_event = MagicMock(spec=KernelProcessEvent)
|
||||
inner_event.id = "event_001"
|
||||
inner_event.data = {"key": "value"}
|
||||
inner_event.visibility = KernelProcessEventVisibility.Public
|
||||
|
||||
# Act
|
||||
local_event = LocalEvent(namespace=namespace, inner_event=inner_event)
|
||||
|
||||
# Assert
|
||||
assert local_event.namespace == namespace
|
||||
assert local_event.inner_event == inner_event
|
||||
assert local_event.id == f"{namespace}.event_001"
|
||||
assert local_event.data == {"key": "value"}
|
||||
assert local_event.visibility == KernelProcessEventVisibility.Public
|
||||
|
||||
|
||||
def test_initialization_without_namespace():
|
||||
# Arrange
|
||||
namespace = None
|
||||
inner_event = MagicMock(spec=KernelProcessEvent)
|
||||
inner_event.id = "event_002"
|
||||
inner_event.data = {"key": "value"}
|
||||
inner_event.visibility = KernelProcessEventVisibility.Internal
|
||||
|
||||
# Act
|
||||
local_event = LocalEvent(namespace=namespace, inner_event=inner_event)
|
||||
|
||||
# Assert
|
||||
assert local_event.namespace is None
|
||||
assert local_event.inner_event == inner_event
|
||||
assert local_event.id == "event_002"
|
||||
assert local_event.data == {"key": "value"}
|
||||
assert local_event.visibility == KernelProcessEventVisibility.Internal
|
||||
|
||||
|
||||
def test_from_kernel_process_event():
|
||||
# Arrange
|
||||
namespace = "namespace_from_method"
|
||||
kernel_process_event = MagicMock(spec=KernelProcessEvent)
|
||||
kernel_process_event.id = "event_003"
|
||||
kernel_process_event.data = {"key": "value"}
|
||||
kernel_process_event.visibility = KernelProcessEventVisibility.Public
|
||||
|
||||
# Act
|
||||
local_event = LocalEvent.from_kernel_process_event(kernel_process_event=kernel_process_event, namespace=namespace)
|
||||
|
||||
# Assert
|
||||
assert local_event.namespace == namespace
|
||||
assert local_event.inner_event == kernel_process_event
|
||||
assert local_event.id == f"{namespace}.event_003"
|
||||
assert local_event.data == {"key": "value"}
|
||||
assert local_event.visibility == KernelProcessEventVisibility.Public
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
|
||||
from semantic_kernel.processes.local_runtime.local_kernel_process import start
|
||||
from semantic_kernel.processes.local_runtime.local_kernel_process_context import LocalKernelProcessContext
|
||||
|
||||
|
||||
async def test_start_method():
|
||||
# Arrange
|
||||
state = MagicMock(spec=KernelProcessState)
|
||||
state.name = "startable_state"
|
||||
state.id = "test_id"
|
||||
steps = [MagicMock(spec=KernelProcessStepInfo)]
|
||||
process = KernelProcess(state=state, steps=steps)
|
||||
kernel = MagicMock(spec=Kernel)
|
||||
initial_event = MagicMock(spec=KernelProcessEvent)
|
||||
|
||||
with (
|
||||
patch.object(LocalKernelProcessContext, "start_with_event", new=AsyncMock()) as mock_start_with_event,
|
||||
patch(
|
||||
"semantic_kernel.processes.local_runtime.local_kernel_process_context.LocalKernelProcessContext",
|
||||
autospec=True,
|
||||
) as MockContext,
|
||||
):
|
||||
mock_context_instance = MockContext.return_value
|
||||
mock_context_instance.__aenter__.return_value = mock_context_instance
|
||||
mock_context_instance.__aexit__ = AsyncMock()
|
||||
|
||||
# Act
|
||||
result = await start(process=process, kernel=kernel, initial_event=initial_event)
|
||||
|
||||
# Assert
|
||||
mock_start_with_event.assert_awaited_once_with(initial_event)
|
||||
assert isinstance(result, LocalKernelProcessContext)
|
||||
|
||||
|
||||
async def test_failed_start():
|
||||
state = MagicMock(spec=KernelProcessState)
|
||||
state.name = "startable_state"
|
||||
state.id = "test_id"
|
||||
steps = [MagicMock(spec=KernelProcessStepInfo)]
|
||||
process = KernelProcess(state=state, steps=steps)
|
||||
kernel = MagicMock(spec=Kernel)
|
||||
initial_event = MagicMock(spec=KernelProcessEvent)
|
||||
|
||||
with pytest.raises(ProcessInvalidConfigurationException):
|
||||
_ = await start(process=None, kernel=kernel, initial_event=initial_event)
|
||||
|
||||
with pytest.raises(ProcessInvalidConfigurationException):
|
||||
_ = await start(process=process, kernel=None, initial_event=initial_event)
|
||||
|
||||
with pytest.raises(ProcessInvalidConfigurationException):
|
||||
_ = await start(process=process, kernel=kernel, initial_event=None)
|
||||
@@ -0,0 +1,136 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
|
||||
from semantic_kernel.processes.local_runtime.local_event import KernelProcessEvent
|
||||
from semantic_kernel.processes.local_runtime.local_kernel_process_context import LocalKernelProcessContext
|
||||
from semantic_kernel.processes.local_runtime.local_process import LocalProcess
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_kernel():
|
||||
return MagicMock(spec=Kernel)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_process():
|
||||
state = MagicMock(spec=KernelProcessState)
|
||||
state.name = "test_process"
|
||||
step_info = MagicMock(spec=KernelProcessStepInfo)
|
||||
process = MagicMock(spec=KernelProcess)
|
||||
process.state = state
|
||||
process.steps = [step_info]
|
||||
process.factories = {}
|
||||
return process
|
||||
|
||||
|
||||
async def test_initialization(mock_process, mock_kernel):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
|
||||
# Act
|
||||
context = LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Assert
|
||||
assert context.local_process is not None
|
||||
|
||||
|
||||
async def test_initialization_with_missing_kernel_throws(mock_process):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="Kernel must be provided"):
|
||||
_ = LocalKernelProcessContext(process=mock_process, kernel=None)
|
||||
|
||||
|
||||
def test_initialization_raises_value_error_for_missing_process_state(mock_kernel):
|
||||
# Arrange
|
||||
mock_process = MagicMock(spec=KernelProcess)
|
||||
mock_process.state = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="Process and process state must be provided and have a valid name"):
|
||||
LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
|
||||
async def test_start_with_event(mock_process, mock_kernel):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
context = LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
event = MagicMock(spec=KernelProcessEvent)
|
||||
|
||||
# Patch the 'run_once' method of LocalProcess.
|
||||
with patch.object(LocalProcess, "run_once", new=AsyncMock()) as mock_run_once:
|
||||
# Act
|
||||
await context.start_with_event(initial_event=event)
|
||||
|
||||
# Assert
|
||||
mock_run_once.assert_awaited_once_with(event)
|
||||
|
||||
|
||||
async def test_send_event(mock_process, mock_kernel):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
context = LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
event = MagicMock(spec=KernelProcessEvent)
|
||||
|
||||
# Patch the 'send_message' method of LocalProcess.
|
||||
with patch.object(LocalProcess, "send_message", new=AsyncMock()) as mock_send_message:
|
||||
# Act
|
||||
await context.send_event(event)
|
||||
|
||||
# Assert
|
||||
mock_send_message.assert_awaited_once_with(event)
|
||||
|
||||
|
||||
async def test_stop(mock_process, mock_kernel):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
context = LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Patch the 'stop' method of LocalProcess.
|
||||
with patch.object(LocalProcess, "stop", new=AsyncMock()) as mock_stop:
|
||||
# Act
|
||||
await context.stop()
|
||||
|
||||
# Assert
|
||||
mock_stop.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_get_state(mock_process, mock_kernel):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
context = LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Patch the 'get_process_info' method of LocalProcess.
|
||||
with patch.object(
|
||||
LocalProcess, "get_process_info", new=AsyncMock(return_value=mock_process)
|
||||
) as mock_get_process_info:
|
||||
# Act
|
||||
result = await context.get_state()
|
||||
|
||||
# Assert
|
||||
assert result == mock_process
|
||||
mock_get_process_info.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_async_context_manager(mock_process, mock_kernel):
|
||||
# Arrange
|
||||
mock_process.state.id = "test_id"
|
||||
context = LocalKernelProcessContext(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Patch the 'dispose' method of the LocalKernelProcessContext class itself.
|
||||
with patch.object(LocalKernelProcessContext, "dispose", new=AsyncMock()) as mock_dispose:
|
||||
# Act
|
||||
async with context as ctx:
|
||||
assert ctx == context
|
||||
|
||||
# Assert
|
||||
mock_dispose.assert_awaited_once()
|
||||
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.processes.local_runtime.local_message import LocalMessage
|
||||
|
||||
|
||||
def test_initialization_with_all_fields():
|
||||
# Arrange
|
||||
source_id = "source_001"
|
||||
destination_id = "destination_001"
|
||||
function_name = "process_data"
|
||||
values = {"param": "value"}
|
||||
target_event_id = "event_001"
|
||||
target_event_data = {"key": "value"}
|
||||
|
||||
# Act
|
||||
message = LocalMessage(
|
||||
source_id=source_id,
|
||||
destination_id=destination_id,
|
||||
function_name=function_name,
|
||||
values=values,
|
||||
target_event_id=target_event_id,
|
||||
target_event_data=target_event_data,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert message.source_id == source_id
|
||||
assert message.destination_id == destination_id
|
||||
assert message.function_name == function_name
|
||||
assert message.values == values
|
||||
assert message.target_event_id == target_event_id
|
||||
assert message.target_event_data == target_event_data
|
||||
|
||||
|
||||
def test_initialization_with_required_fields_only():
|
||||
# Arrange
|
||||
source_id = "source_002"
|
||||
destination_id = "destination_002"
|
||||
function_name = "execute_task"
|
||||
values = {"input": "data"}
|
||||
|
||||
# Act
|
||||
message = LocalMessage(
|
||||
source_id=source_id, destination_id=destination_id, function_name=function_name, values=values
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert message.source_id == source_id
|
||||
assert message.destination_id == destination_id
|
||||
assert message.function_name == function_name
|
||||
assert message.values == values
|
||||
assert message.target_event_id is None
|
||||
assert message.target_event_data is None
|
||||
|
||||
|
||||
def test_initialization_with_empty_values():
|
||||
# Arrange
|
||||
source_id = "source_003"
|
||||
destination_id = "destination_003"
|
||||
function_name = "no_op"
|
||||
values = {}
|
||||
|
||||
# Act
|
||||
message = LocalMessage(
|
||||
source_id=source_id, destination_id=destination_id, function_name=function_name, values=values
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert message.source_id == source_id
|
||||
assert message.destination_id == destination_id
|
||||
assert message.function_name == function_name
|
||||
assert message.values == values
|
||||
assert message.target_event_id is None
|
||||
assert message.target_event_data is None
|
||||
|
||||
|
||||
def test_initialization_invalid_source_id():
|
||||
# Arrange
|
||||
source_id = None # Invalid type
|
||||
destination_id = "destination_004"
|
||||
function_name = "invalid_test"
|
||||
values = {"input": "data"}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError):
|
||||
LocalMessage(source_id=source_id, destination_id=destination_id, function_name=function_name, values=values)
|
||||
|
||||
|
||||
def test_initialization_invalid_destination_id():
|
||||
# Arrange
|
||||
source_id = "source_004"
|
||||
destination_id = None # Invalid type
|
||||
function_name = "invalid_test"
|
||||
values = {"input": "data"}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError):
|
||||
LocalMessage(source_id=source_id, destination_id=destination_id, function_name=function_name, values=values)
|
||||
|
||||
|
||||
def test_initialization_invalid_function_name():
|
||||
# Arrange
|
||||
source_id = "source_005"
|
||||
destination_id = "destination_005"
|
||||
function_name = None # Invalid type
|
||||
values = {"input": "data"}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError):
|
||||
LocalMessage(source_id=source_id, destination_id=destination_id, function_name=function_name, values=values)
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
|
||||
from semantic_kernel.processes.local_runtime.local_message import LocalMessage
|
||||
from semantic_kernel.processes.local_runtime.local_message_factory import LocalMessageFactory
|
||||
|
||||
|
||||
def test_create_from_edge_with_data():
|
||||
# Arrange
|
||||
edge = MagicMock(spec=KernelProcessEdge)
|
||||
edge.source_step_id = "source_001"
|
||||
edge.output_target = KernelProcessFunctionTarget(
|
||||
step_id="target_001", function_name="process_data", parameter_name="input_data", target_event_id="event_123"
|
||||
)
|
||||
data = {"key": "value"}
|
||||
|
||||
# Act
|
||||
message = LocalMessageFactory.create_from_edge(edge=edge, data=data)
|
||||
|
||||
# Assert
|
||||
assert isinstance(message, LocalMessage)
|
||||
assert message.source_id == "source_001"
|
||||
assert message.destination_id == "target_001"
|
||||
assert message.function_name == "process_data"
|
||||
assert message.values == {"input_data": data}
|
||||
assert message.target_event_id == "event_123"
|
||||
assert message.target_event_data == data
|
||||
|
||||
|
||||
def test_create_from_edge_without_parameter_name():
|
||||
# Arrange
|
||||
edge = MagicMock(spec=KernelProcessEdge)
|
||||
edge.source_step_id = "source_002"
|
||||
edge.output_target = KernelProcessFunctionTarget(
|
||||
step_id="target_002", function_name="execute_task", parameter_name=None, target_event_id="event_456"
|
||||
)
|
||||
data = {"another_key": "another_value"}
|
||||
|
||||
# Act
|
||||
message = LocalMessageFactory.create_from_edge(edge=edge, data=data)
|
||||
|
||||
# Assert
|
||||
assert isinstance(message, LocalMessage)
|
||||
assert message.source_id == "source_002"
|
||||
assert message.destination_id == "target_002"
|
||||
assert message.function_name == "execute_task"
|
||||
assert message.values == {} # No parameter name, so values should be empty
|
||||
assert message.target_event_id == "event_456"
|
||||
assert message.target_event_data == data
|
||||
|
||||
|
||||
def test_create_from_edge_with_no_data():
|
||||
# Arrange
|
||||
edge = MagicMock(spec=KernelProcessEdge)
|
||||
edge.source_step_id = "source_003"
|
||||
edge.output_target = KernelProcessFunctionTarget(
|
||||
step_id="target_003", function_name="perform_action", parameter_name="param_name", target_event_id="event_789"
|
||||
)
|
||||
data = None
|
||||
|
||||
# Act
|
||||
message = LocalMessageFactory.create_from_edge(edge=edge, data=data)
|
||||
|
||||
# Assert
|
||||
assert isinstance(message, LocalMessage)
|
||||
assert message.source_id == "source_003"
|
||||
assert message.destination_id == "target_003"
|
||||
assert message.function_name == "perform_action"
|
||||
assert message.values == {"param_name": None} # Parameter name exists but data is None
|
||||
assert message.target_event_id == "event_789"
|
||||
assert message.target_event_data is None
|
||||
|
||||
|
||||
def test_create_from_edge_invalid_edge():
|
||||
# Arrange
|
||||
edge = None
|
||||
data = {"key": "value"}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(AttributeError):
|
||||
LocalMessageFactory.create_from_edge(edge=edge, data=data)
|
||||
@@ -0,0 +1,358 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.exceptions.kernel_exceptions import KernelException
|
||||
from semantic_kernel.exceptions.process_exceptions import ProcessEventUndefinedException
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_event import (
|
||||
KernelProcessEvent,
|
||||
KernelProcessEventVisibility,
|
||||
)
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
|
||||
from semantic_kernel.processes.local_runtime.local_message import LocalMessage
|
||||
from semantic_kernel.processes.local_runtime.local_process import LocalProcess
|
||||
from semantic_kernel.processes.local_runtime.local_step import LocalStep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_kernel():
|
||||
return MagicMock(spec=Kernel)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_process_with_output_edges(mock_process, mock_kernel, build_model):
|
||||
"""Fixture to create a LocalProcess with predefined output edges."""
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
local_process.output_edges = {"valid_event_id": MagicMock()}
|
||||
return local_process
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_process(mock_kernel):
|
||||
state = MagicMock(spec=KernelProcessState)
|
||||
state.name = "test_process"
|
||||
state.id = "process_001"
|
||||
step_info = MagicMock(spec=KernelProcessStepInfo)
|
||||
step_info.state = state
|
||||
process = MagicMock(spec=KernelProcess)
|
||||
process.state = state
|
||||
process.steps = [step_info]
|
||||
process.edges = {"event_001": [MagicMock(spec=KernelProcessEdge)]}
|
||||
return process
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_local_step():
|
||||
step_info = MagicMock(spec=KernelProcessStepInfo)
|
||||
step_info.state.name = "step_name"
|
||||
step_info.state.id = "step_001"
|
||||
local_step = MagicMock(spec=LocalProcess)
|
||||
local_step.step_info = step_info
|
||||
local_step.id = "step_001"
|
||||
return local_step
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def build_model():
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess # noqa: F401
|
||||
|
||||
LocalProcess.model_rebuild()
|
||||
|
||||
|
||||
def test_initialization(mock_process, mock_kernel, build_model):
|
||||
# Act
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Assert
|
||||
assert local_process.process == mock_process
|
||||
assert local_process.kernel == mock_kernel
|
||||
assert not local_process.initialize_task
|
||||
assert len(local_process.step_infos) == len(mock_process.steps)
|
||||
assert local_process.step_infos[0] == mock_process.steps[0]
|
||||
assert local_process.external_event_queue.empty()
|
||||
|
||||
|
||||
def test_initialization_max_supersteps(mock_process, mock_kernel, build_model):
|
||||
# Act
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel, max_supersteps=10)
|
||||
|
||||
# Assert
|
||||
assert local_process.process == mock_process
|
||||
assert local_process.kernel == mock_kernel
|
||||
assert not local_process.initialize_task
|
||||
assert len(local_process.step_infos) == len(mock_process.steps)
|
||||
assert local_process.step_infos[0] == mock_process.steps[0]
|
||||
assert local_process.external_event_queue.empty()
|
||||
assert local_process.max_supersteps == 10
|
||||
|
||||
|
||||
def test_ensure_initialized(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
with patch.object(
|
||||
LocalProcess, "initialize_process", wraps=local_process.initialize_process
|
||||
) as mock_initialize_process:
|
||||
# Act
|
||||
local_process.ensure_initialized()
|
||||
|
||||
# Assert
|
||||
mock_initialize_process.assert_called_once()
|
||||
assert local_process.initialize_task is True
|
||||
|
||||
|
||||
def test_ensure_initialized_already_done(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
local_process.initialize_task = True # Simulate that the process is already initialized
|
||||
|
||||
with patch.object(
|
||||
LocalProcess, "initialize_process", wraps=local_process.initialize_process
|
||||
) as mock_initialize_process:
|
||||
# Act
|
||||
local_process.ensure_initialized()
|
||||
|
||||
# Assert
|
||||
mock_initialize_process.assert_not_called()
|
||||
|
||||
|
||||
async def test_start(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Patch `ensure_initialized` to track its call but still run the original method.
|
||||
with (
|
||||
patch.object(
|
||||
LocalProcess, "ensure_initialized", wraps=local_process.ensure_initialized
|
||||
) as mock_ensure_initialized,
|
||||
patch.object(LocalProcess, "internal_execute", new_callable=AsyncMock),
|
||||
):
|
||||
# Act
|
||||
await local_process.start(keep_alive=True)
|
||||
|
||||
# Assert
|
||||
mock_ensure_initialized.assert_called_once()
|
||||
assert local_process.process_task is not None
|
||||
assert isinstance(local_process.process_task, asyncio.Task)
|
||||
|
||||
|
||||
async def test_run_once(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
local_process.process_task = AsyncMock(spec=asyncio.Task)
|
||||
|
||||
# Patch `ensure_initialized` to track its call but still run the original method.
|
||||
with (
|
||||
patch.object(LocalProcess, "ensure_initialized", wraps=local_process.ensure_initialized),
|
||||
patch.object(LocalProcess, "start", new_callable=AsyncMock),
|
||||
):
|
||||
# Act
|
||||
await local_process.start(keep_alive=True)
|
||||
|
||||
# Assert
|
||||
assert local_process.process_task is not None
|
||||
assert isinstance(local_process.process_task, asyncio.Task)
|
||||
|
||||
|
||||
async def test_stop_running_task(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
local_process.process_task = asyncio.create_task(asyncio.sleep(1)) # Mock a running task
|
||||
|
||||
# Act
|
||||
await local_process.stop()
|
||||
|
||||
# Assert
|
||||
assert local_process.process_task.cancelled()
|
||||
|
||||
|
||||
async def test_stop_no_running_task(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
local_process.process_task = None
|
||||
|
||||
# Act
|
||||
await local_process.stop()
|
||||
|
||||
# Assert
|
||||
assert local_process.process_task is None # No action should be taken
|
||||
|
||||
|
||||
async def test_send_message(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
process_event = MagicMock(spec=KernelProcessEvent)
|
||||
|
||||
# Act
|
||||
await local_process.send_message(process_event)
|
||||
|
||||
# Assert
|
||||
assert not local_process.external_event_queue.empty()
|
||||
assert local_process.external_event_queue.get() == process_event
|
||||
|
||||
|
||||
async def test_send_message_with_invalid_event(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ProcessEventUndefinedException):
|
||||
await local_process.send_message(None)
|
||||
|
||||
|
||||
async def test_run_once_with_valid_event(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
process_event = MagicMock(spec=KernelProcessEvent)
|
||||
|
||||
async def mock_coroutine():
|
||||
return None
|
||||
|
||||
task = asyncio.create_task(mock_coroutine())
|
||||
local_process.process_task = task
|
||||
|
||||
with patch.object(LocalProcess, "start", new_callable=AsyncMock) as mock_start:
|
||||
# Act
|
||||
await local_process.run_once(process_event)
|
||||
|
||||
# Assert
|
||||
assert not local_process.external_event_queue.empty()
|
||||
assert local_process.external_event_queue.get() == process_event
|
||||
mock_start.assert_awaited_once_with(keep_alive=False)
|
||||
assert local_process.process_task.done()
|
||||
|
||||
|
||||
async def test_run_once_with_no_event_raises_exception(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ProcessEventUndefinedException):
|
||||
await local_process.run_once(None)
|
||||
|
||||
|
||||
async def test_run_once_without_process_task(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
process_event = MagicMock(spec=KernelProcessEvent)
|
||||
local_process.process_task = None # Simulate no process task
|
||||
|
||||
with patch.object(LocalProcess, "start", new_callable=AsyncMock) as mock_start:
|
||||
# Act
|
||||
await local_process.run_once(process_event)
|
||||
|
||||
# Assert
|
||||
assert not local_process.external_event_queue.empty()
|
||||
assert local_process.external_event_queue.get() == process_event
|
||||
mock_start.assert_awaited_once_with(keep_alive=False)
|
||||
assert local_process.process_task is None
|
||||
|
||||
|
||||
def test_initialize_process(mock_process, mock_kernel, build_model):
|
||||
# Arrange
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.processes.local_runtime.local_process.LocalProcess.__init__", return_value=None
|
||||
) as mock_local_process_init,
|
||||
patch(
|
||||
"semantic_kernel.processes.local_runtime.local_step.LocalStep.__init__", return_value=None
|
||||
) as mock_local_step_init,
|
||||
):
|
||||
# Act
|
||||
local_process.initialize_process()
|
||||
|
||||
# Assert
|
||||
assert local_process.output_edges == {"event_001": list(mock_process.edges["event_001"])}
|
||||
|
||||
for step_info in mock_process.steps:
|
||||
if isinstance(step_info, KernelProcess):
|
||||
mock_local_process_init.assert_called_with(
|
||||
process=step_info,
|
||||
kernel=mock_kernel,
|
||||
parent_process_id=local_process.id,
|
||||
)
|
||||
else:
|
||||
mock_local_step_init.assert_called_with(
|
||||
step_info=step_info,
|
||||
kernel=mock_kernel,
|
||||
factories={},
|
||||
parent_process_id=local_process.id,
|
||||
)
|
||||
|
||||
assert len(local_process.steps) == len(mock_process.steps)
|
||||
|
||||
|
||||
async def test_handle_message_without_target_event_id(mock_process_with_output_edges, build_model):
|
||||
# Arrange
|
||||
local_process = mock_process_with_output_edges
|
||||
message = MagicMock(spec=LocalMessage)
|
||||
message.target_event_id = None # Simulate missing target_event_id
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(KernelException) as exc_info:
|
||||
await local_process.handle_message(message)
|
||||
|
||||
# Verify that the exception message is correct
|
||||
assert "The target event id must be specified" in str(exc_info.value)
|
||||
|
||||
|
||||
async def test_handle_message_with_valid_event_id(mock_process_with_output_edges, build_model):
|
||||
# Arrange
|
||||
local_process = mock_process_with_output_edges
|
||||
message = MagicMock(spec=LocalMessage)
|
||||
message.target_event_id = "valid_event_id"
|
||||
message.target_event_data = {"key": "value"} # Simulate message data
|
||||
|
||||
# Patch the run_once method on the LocalProcess class to prevent actual execution and track the call.
|
||||
with patch.object(LocalProcess, "run_once", new_callable=AsyncMock) as mock_run_once:
|
||||
# Act
|
||||
await local_process.handle_message(message)
|
||||
|
||||
# Assert
|
||||
# Verify that run_once was called with the correct KernelProcessEvent
|
||||
mock_run_once.assert_awaited_once()
|
||||
event = mock_run_once.call_args[0][0]
|
||||
assert isinstance(event, KernelProcessEvent)
|
||||
assert event.id == "valid_event_id"
|
||||
assert event.data == message.target_event_data
|
||||
assert event.visibility == KernelProcessEventVisibility.Internal
|
||||
|
||||
|
||||
END_PROCESS_ID = "END"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_process_with_steps(mock_process, mock_kernel, build_model):
|
||||
"""Fixture to create a LocalProcess with mock steps."""
|
||||
local_process = LocalProcess(process=mock_process, kernel=mock_kernel)
|
||||
|
||||
# Create real LocalStep instances
|
||||
state_1 = MagicMock(spec=KernelProcessState)
|
||||
state_1.name = "step_1_state"
|
||||
state_1.id = "step_1_id"
|
||||
|
||||
step_info_1 = MagicMock(spec=KernelProcessStepInfo)
|
||||
step_info_1.state = state_1
|
||||
|
||||
state_2 = MagicMock(spec=KernelProcessState)
|
||||
state_2.name = "step_2_state"
|
||||
state_2.id = "step_2_id"
|
||||
|
||||
step_info_2 = MagicMock(spec=KernelProcessStepInfo)
|
||||
step_info_2.state = state_2
|
||||
|
||||
step_1 = LocalStep(step_info=step_info_1, kernel=mock_kernel, parent_process_id="test_process")
|
||||
step_2 = LocalStep(step_info=step_info_2, kernel=mock_kernel, parent_process_id="test_process")
|
||||
|
||||
local_process.steps = [step_1, step_2]
|
||||
return local_process
|
||||
@@ -0,0 +1,564 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
from queue import Queue
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel import Kernel
|
||||
from semantic_kernel.exceptions.kernel_exceptions import KernelException
|
||||
from semantic_kernel.exceptions.process_exceptions import (
|
||||
ProcessFunctionNotFoundException,
|
||||
)
|
||||
from semantic_kernel.functions import KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
|
||||
from semantic_kernel.processes.local_runtime.local_event import LocalEvent
|
||||
from semantic_kernel.processes.local_runtime.local_message import LocalMessage
|
||||
from semantic_kernel.processes.local_runtime.local_step import LocalStep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_process_step_state():
|
||||
"""Fixture for creating a mocked KernelProcessStepState object."""
|
||||
return KernelProcessStepState(name="my_step", id="123", state=None, version="1.0")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_process_step_state_without_id():
|
||||
"""Fixture for creating a mocked KernelProcessStepState object without id."""
|
||||
return KernelProcessStepState(name="my_step", id=None, state=None, version="1.0")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_process_step_info(mocked_process_step_state):
|
||||
"""Fixture for creating a mocked KernelProcessStepInfo object."""
|
||||
return KernelProcessStepInfo(inner_step_type=AsyncMock(spec=type), state=mocked_process_step_state, output_edges={})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_process_step_info_without_id(mocked_process_step_state_without_id):
|
||||
"""Fixture for creating a mocked KernelProcessStepInfo object."""
|
||||
return KernelProcessStepInfo(
|
||||
inner_step_type=AsyncMock(spec=type), state=mocked_process_step_state_without_id, output_edges={}
|
||||
)
|
||||
|
||||
|
||||
async def test_parse_initial_configuration_adds_ids_if_missing():
|
||||
"""Test that parse_initial_configuration sets the step_info.state.id if it is None."""
|
||||
edge = KernelProcessEdge(source_step_id="s1", output_target=AsyncMock(spec=KernelProcessFunctionTarget))
|
||||
data = {
|
||||
"step_info": KernelProcess(
|
||||
state=KernelProcessState(name="test_step", version="1.0"), # noqa: F821
|
||||
steps=[AsyncMock(spec=KernelProcessStepInfo)],
|
||||
edges={"test_event": [edge]},
|
||||
),
|
||||
}
|
||||
|
||||
# Call the parse_initial_configuration
|
||||
processed_data = LocalStep.parse_initial_configuration(data) # type: ignore
|
||||
|
||||
# Assert that it sets the id
|
||||
assert processed_data["step_state"].id is not None
|
||||
assert processed_data["step_state"].id != ""
|
||||
assert processed_data["event_namespace"] == f"{processed_data['step_state'].name}_{processed_data['step_state'].id}"
|
||||
assert "output_edges" in processed_data
|
||||
assert processed_data["output_edges"] == {"test_event": [edge]}
|
||||
|
||||
|
||||
async def test_parse_initial_configuration_id_already_set():
|
||||
"""Test that parse_initial_configuration does not overwrite step_info.state.id if already set."""
|
||||
data = {
|
||||
"step_info": KernelProcess(
|
||||
state=KernelProcessState(name="test_step", id="test_id_set", version="1.0"), # noqa: F821
|
||||
steps=[AsyncMock(spec=KernelProcessStepInfo)],
|
||||
),
|
||||
}
|
||||
|
||||
# Call the parse_initial_configuration
|
||||
processed_data = LocalStep.parse_initial_configuration(data) # type: ignore
|
||||
|
||||
assert processed_data["step_state"].id is not None
|
||||
assert processed_data["step_state"].id == "test_id_set"
|
||||
|
||||
assert processed_data["event_namespace"] == f"{processed_data['step_state'].name}_{processed_data['step_state'].id}"
|
||||
assert "output_edges" in processed_data
|
||||
|
||||
|
||||
def test_name_property(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test that the name property returns the name from the step_info.state."""
|
||||
step = LocalStep(
|
||||
kernel=MagicMock(spec=Kernel),
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
assert step.name == "my_step"
|
||||
|
||||
|
||||
def test_id_property_with_id(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test that the id property returns the ID if it is available."""
|
||||
step = LocalStep(
|
||||
kernel=MagicMock(spec=Kernel),
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
assert step.id == "123"
|
||||
|
||||
|
||||
def test_id_property_without_id(mocked_process_step_state_without_id, mocked_process_step_info_without_id):
|
||||
"""Test that the id property returns an empty string if ID is None."""
|
||||
step = LocalStep(
|
||||
kernel=MagicMock(spec=Kernel),
|
||||
step_info=mocked_process_step_info_without_id,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state_without_id,
|
||||
factories={},
|
||||
)
|
||||
|
||||
assert step.id == ""
|
||||
|
||||
|
||||
async def test_handle_message_raises_exception_when_message_is_none(
|
||||
mocked_process_step_state, mocked_process_step_info
|
||||
):
|
||||
"""Test handle_message raises ValueError when message is None."""
|
||||
step = LocalStep(
|
||||
kernel=MagicMock(spec=Kernel),
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
await step.handle_message(cast(LocalMessage, None))
|
||||
assert "The message is None." in str(exc.value)
|
||||
|
||||
|
||||
async def test_handle_message_initializes_step_if_not_initialized(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test handle_message calls initialize_step if the step isn't yet initialized."""
|
||||
mock_kernel = MagicMock(spec=Kernel)
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel,
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
functions={"other_func": AsyncMock(spec=KernelFunction)},
|
||||
inputs={
|
||||
"other_func": {"param": "ready_value"},
|
||||
},
|
||||
initial_inputs={
|
||||
"other_func": {"param": None},
|
||||
},
|
||||
initialize_task=False,
|
||||
)
|
||||
|
||||
with patch.object(LocalStep, "initialize_step") as mock_initialize_step:
|
||||
msg = LocalMessage(
|
||||
source_id="source",
|
||||
destination_id="dest",
|
||||
function_name="other_func",
|
||||
values={"param": "value"},
|
||||
target_event_id=None,
|
||||
target_event_data=None,
|
||||
)
|
||||
|
||||
await step.handle_message(msg)
|
||||
|
||||
mock_initialize_step.assert_awaited_once()
|
||||
assert step.initialize_task is True
|
||||
|
||||
|
||||
async def test_handle_message_raises_if_functions_not_initialized(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test handle_message raises ValueError if step is not properly initialized."""
|
||||
# We simulate that after initialization, the step still doesn't have `functions`.
|
||||
mock_kernel = AsyncMock(spec=Kernel)
|
||||
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel, # type: ignore
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
initialize_task=False,
|
||||
)
|
||||
|
||||
# Force the initialize_step to not fill in functions.
|
||||
async def mocked_init_step():
|
||||
step.functions = {}
|
||||
step.inputs = {}
|
||||
step.initial_inputs = {}
|
||||
|
||||
with patch.object(
|
||||
LocalStep,
|
||||
"initialize_step",
|
||||
return_value=AsyncMock(side_effect=mocked_init_step),
|
||||
):
|
||||
msg = LocalMessage(
|
||||
source_id="source",
|
||||
destination_id="dest",
|
||||
function_name="any_func",
|
||||
values={"param": "value"},
|
||||
)
|
||||
|
||||
with pytest.raises(ProcessFunctionNotFoundException) as exc:
|
||||
await step.handle_message(msg)
|
||||
|
||||
assert "Function any_func not found in plugin my_step" in str(exc.value)
|
||||
|
||||
|
||||
async def test_handle_message_updates_inputs_and_invokes_function(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test that handle_message updates inputs with message values and invokes the function
|
||||
if all parameters are provided."""
|
||||
mock_kernel = AsyncMock(spec=Kernel)
|
||||
mock_kernel.invoke = AsyncMock(return_value=MagicMock(value="result"))
|
||||
|
||||
# Create a function that requires one parameter
|
||||
mock_function = AsyncMock(spec=KernelFunction)
|
||||
mock_function.name = "func"
|
||||
mock_function.plugin_name = "test_plugin"
|
||||
|
||||
mock_function.metadata = AsyncMock(spec=KernelFunctionMetadata)
|
||||
mock_function.metadata.name = "func"
|
||||
mock_function.metadata.plugin_name = "test_plugin"
|
||||
mock_function.metadata.is_prompt = False
|
||||
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel,
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
functions={
|
||||
"func": mock_function,
|
||||
},
|
||||
inputs={"func": {"param": None}},
|
||||
initial_inputs={"func": {"param": None}},
|
||||
initialize_task=True,
|
||||
)
|
||||
|
||||
with patch.object(LocalStep, "emit_event") as mock_emit_event:
|
||||
msg = LocalMessage(
|
||||
source_id="source",
|
||||
destination_id="dest",
|
||||
function_name="func",
|
||||
values={"param": "value"},
|
||||
)
|
||||
|
||||
await step.handle_message(msg)
|
||||
|
||||
# Function invoked with correct arguments
|
||||
mock_kernel.invoke.assert_awaited_once()
|
||||
mock_emit_event.assert_awaited()
|
||||
|
||||
assert mock_emit_event.call_args.args[0].id == "func.OnResult"
|
||||
|
||||
# After invocation, input is reset
|
||||
assert step.inputs["func"]["param"] is None
|
||||
|
||||
|
||||
async def test_handle_message_raises_target_function_not_found(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test handle_message raises an exception if the target function is not the one that is invocable."""
|
||||
mock_kernel = AsyncMock(spec=Kernel)
|
||||
mock_kernel.invoke = AsyncMock(return_value=AsyncMock(value="result"))
|
||||
|
||||
# Pretend we have two functions, and only "other_func" is fully ready
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel,
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state, # type: ignore
|
||||
factories={},
|
||||
functions={"other_func": AsyncMock(spec=KernelFunction)},
|
||||
inputs={
|
||||
"other_func": {"param": "ready_value"},
|
||||
},
|
||||
initial_inputs={
|
||||
"other_func": {"param": None},
|
||||
},
|
||||
initialize_task=True,
|
||||
)
|
||||
|
||||
msg = LocalMessage(
|
||||
source_id="source",
|
||||
destination_id="dest",
|
||||
function_name="mismatched_func",
|
||||
values={"param": "value"},
|
||||
)
|
||||
|
||||
with pytest.raises(ProcessFunctionNotFoundException) as exc:
|
||||
await step.handle_message(msg)
|
||||
|
||||
assert "Function mismatched_func not found in plugin my_step" in str(exc.value)
|
||||
|
||||
|
||||
async def test_handle_message_raises_function_not_found_if_no_function(
|
||||
mocked_process_step_state, mocked_process_step_info
|
||||
):
|
||||
"""Test handle_message raises ProcessFunctionNotFoundException if the function is not found in the step."""
|
||||
mock_kernel = AsyncMock(spec=Kernel)
|
||||
mock_kernel.invoke = AsyncMock(return_value=AsyncMock(value="result"))
|
||||
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel,
|
||||
step_info=mocked_process_step_info, # type: ignore
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
functions={},
|
||||
inputs={"func": {"param": "ready_value"}},
|
||||
initial_inputs={"func": {"param": None}},
|
||||
initialize_task=True,
|
||||
)
|
||||
|
||||
msg = LocalMessage(
|
||||
source_id="source",
|
||||
destination_id="dest",
|
||||
function_name="func",
|
||||
values={"param": "value"},
|
||||
)
|
||||
|
||||
with pytest.raises(ProcessFunctionNotFoundException) as exc:
|
||||
await step.handle_message(msg)
|
||||
assert "Function func not found in plugin my_step" in str(exc.value)
|
||||
|
||||
|
||||
async def test_handle_message_emits_error_event_on_exception(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test handle_message emits an OnError event when the function invocation raises an exception."""
|
||||
mock_kernel = AsyncMock(spec=Kernel)
|
||||
mock_kernel.invoke = AsyncMock(side_effect=KernelException("error"))
|
||||
|
||||
mock_function = AsyncMock(spec=KernelFunction)
|
||||
mock_function.name = "func"
|
||||
mock_function.plugin_name = "test_plugin"
|
||||
|
||||
mock_function.metadata = AsyncMock(spec=KernelFunctionMetadata)
|
||||
|
||||
mock_function.metadata.name = "func"
|
||||
mock_function.metadata.plugin_name = "test_plugin"
|
||||
mock_function.metadata.is_prompt = False
|
||||
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel, # type: ignore
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
functions={"func": mock_function},
|
||||
inputs={"func": {"param": "some_value"}},
|
||||
initial_inputs={"func": {"param": None}},
|
||||
initialize_task=True,
|
||||
)
|
||||
|
||||
with patch.object(LocalStep, "emit_event") as mock_emit_event:
|
||||
msg = LocalMessage(
|
||||
source_id="source",
|
||||
destination_id="dest",
|
||||
function_name="func",
|
||||
values={},
|
||||
)
|
||||
|
||||
await step.handle_message(msg)
|
||||
|
||||
# The event name for error is "func.OnError"
|
||||
assert mock_emit_event.await_args is not None
|
||||
mock_emit_event.assert_awaited()
|
||||
assert mock_emit_event.call_args.args[0].id == "func.OnError"
|
||||
|
||||
|
||||
async def test_invoke_function_calls_kernel_invoke():
|
||||
"""Test invoke_function calls the kernel's invoke method with provided arguments."""
|
||||
mock_kernel = AsyncMock(spec=Kernel)
|
||||
mock_kernel.invoke = AsyncMock()
|
||||
|
||||
mock_function = AsyncMock(spec=KernelFunction)
|
||||
|
||||
mock_step_info = AsyncMock(spec=KernelProcessStepInfo)
|
||||
mock_step_info.edges = MagicMock(return_value={"edge1": "value1", "edge2": "value2"})
|
||||
mock_step_info.state = KernelProcessStepState(name="test", id="step-id", state={}, version="1.0")
|
||||
|
||||
step = LocalStep(
|
||||
kernel=mock_kernel, # type: ignore
|
||||
step_info=mock_step_info,
|
||||
outgoing_event_queue=Queue(),
|
||||
event_namespace="ns",
|
||||
step_state=mock_step_info.state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
args = {"key": "value"}
|
||||
await step.invoke_function(mock_function, mock_kernel, args)
|
||||
|
||||
mock_kernel.invoke.assert_awaited_once_with(mock_function, **args)
|
||||
|
||||
|
||||
async def test_emit_event_puts_local_event_into_queue():
|
||||
"""Test emit_event creates a LocalEvent and puts it into the outgoing_event_queue."""
|
||||
queue_obj = Queue()
|
||||
mock_step_info = AsyncMock(spec=KernelProcessStepInfo)
|
||||
mock_step_info.edges = MagicMock(return_value={"edge1": "value1", "edge2": "value2"})
|
||||
mock_step_info.state = KernelProcessStepState(name="test", id="step-id", state={}, version="1.0")
|
||||
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel),
|
||||
step_info=mock_step_info,
|
||||
outgoing_event_queue=queue_obj,
|
||||
event_namespace="ns",
|
||||
step_state=mock_step_info.state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
event = KernelProcessEvent(id="test_event", data="some_data")
|
||||
await step.emit_event(event)
|
||||
# The queue should contain a LocalEvent
|
||||
assert not queue_obj.empty()
|
||||
local_event = queue_obj.get()
|
||||
assert queue_obj.empty()
|
||||
assert isinstance(local_event, LocalEvent)
|
||||
assert local_event.inner_event is event
|
||||
assert local_event.namespace == "test_step-id"
|
||||
|
||||
|
||||
async def test_emit_local_event_puts_into_queue():
|
||||
"""Test emit_local_event directly places the local_event into the queue with updated namespace."""
|
||||
queue_obj = Queue()
|
||||
mock_step_info = AsyncMock(spec=KernelProcessStepInfo)
|
||||
mock_step_info.edges = MagicMock(return_value={"edge1": "value1", "edge2": "value2"})
|
||||
mock_step_info.state = KernelProcessStepState(name="test", id="step-id", state={}, version="1.0")
|
||||
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel),
|
||||
step_info=mock_step_info,
|
||||
outgoing_event_queue=queue_obj,
|
||||
event_namespace="original_ns",
|
||||
step_state=mock_step_info.state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
local_event = LocalEvent(namespace="temp", inner_event=KernelProcessEvent(id="evt"))
|
||||
await step.emit_local_event(local_event)
|
||||
|
||||
assert not queue_obj.empty()
|
||||
popped = queue_obj.get()
|
||||
assert popped is local_event
|
||||
# The namespace is updated by scoped_event
|
||||
assert popped.namespace == f"{step.name}_{step.id}"
|
||||
|
||||
|
||||
def test_get_all_events_returns_all_events_from_queue(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test get_all_events drains the outgoing_event_queue and returns them."""
|
||||
queue_obj = Queue()
|
||||
event1 = LocalEvent(namespace="ns1", inner_event=KernelProcessEvent(id="e1"))
|
||||
event2 = LocalEvent(namespace="ns2", inner_event=KernelProcessEvent(id="e2"))
|
||||
queue_obj.put(event1)
|
||||
queue_obj.put(event2)
|
||||
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel), # type: ignore
|
||||
step_info=mocked_process_step_info,
|
||||
outgoing_event_queue=queue_obj,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
events = step.get_all_events()
|
||||
assert len(events) == 2
|
||||
assert events[0] == event1
|
||||
assert events[1] == event2
|
||||
# Queue should be empty now
|
||||
assert queue_obj.empty()
|
||||
|
||||
|
||||
def test_get_edge_for_event_returns_edge_list(mocked_process_step_state):
|
||||
"""Test that get_edge_for_event returns the edges from output_edges that match the event id."""
|
||||
edge = KernelProcessEdge(source_step_id="s1", output_target=AsyncMock(spec=KernelProcessFunctionTarget))
|
||||
mock_info = KernelProcessStepInfo(
|
||||
inner_step_type=AsyncMock(spec=type),
|
||||
state=mocked_process_step_state,
|
||||
output_edges={"test_event": [edge]},
|
||||
)
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel), # type: ignore
|
||||
step_info=mock_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
edges = step.get_edge_for_event("test_event")
|
||||
assert len(edges) == 1
|
||||
assert edges[0] == edge
|
||||
|
||||
output = step.output_edges["test_event"]
|
||||
assert output[0] is edge
|
||||
|
||||
# For a non-existing event, expect empty list
|
||||
assert step.get_edge_for_event("not_found") == []
|
||||
|
||||
|
||||
async def test_to_kernel_process_step_info_initializes_if_needed(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test to_kernel_process_step_info calls initialize_step if not yet done."""
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel),
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
initialize_task=False,
|
||||
)
|
||||
|
||||
with patch.object(LocalStep, "initialize_step") as mock_initialize_step:
|
||||
result = await step.to_kernel_process_step_info()
|
||||
|
||||
mock_initialize_step.assert_awaited_once()
|
||||
assert result == mocked_process_step_info
|
||||
assert step.initialize_task is True
|
||||
|
||||
|
||||
def test_scoped_event_updates_namespace(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test scoped_event sets the local_event's namespace to name_id."""
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel),
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
some_event = LocalEvent(namespace=None, inner_event=KernelProcessEvent(id="evt"))
|
||||
result = step.scoped_event(some_event)
|
||||
|
||||
assert result.namespace == "my_step_123"
|
||||
|
||||
|
||||
def test_scoped_event_from_kernel_process_creates_scoped_event(mocked_process_step_state, mocked_process_step_info):
|
||||
"""Test scoped_event_from_kernel_process creates a local event from the kernel process event
|
||||
with the step's scope."""
|
||||
step = LocalStep(
|
||||
kernel=AsyncMock(spec=Kernel), # type: ignore
|
||||
step_info=mocked_process_step_info,
|
||||
event_namespace="ns",
|
||||
step_state=mocked_process_step_state,
|
||||
factories={},
|
||||
)
|
||||
|
||||
kpe = KernelProcessEvent(id="test_id", data="some_data")
|
||||
local_event = step.scoped_event_from_kernel_process(kpe)
|
||||
|
||||
assert local_event.namespace == "my_step_123"
|
||||
assert local_event.inner_event == kpe
|
||||
assert local_event.inner_event is kpe
|
||||
Reference in New Issue
Block a user