359 lines
13 KiB
Python
359 lines
13 KiB
Python
# 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
|