chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,134 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
import pytest
from semantic_kernel.processes.dapr_runtime import (
EventBufferActor,
ExternalEventBufferActor,
MessageBufferActor,
ProcessActor,
StepActor,
)
from semantic_kernel.processes.dapr_runtime.dapr_actor_registration import (
create_actor_factories,
register_fastapi_dapr_actors,
register_flask_dapr_actors,
)
class MockActor:
"""Mock actor to record register_actor calls."""
def __init__(self):
self.registrations = []
async def register_actor(self, actor_type, actor_factory=None):
# Record registration details
self.registrations.append({"actor_type": actor_type, "actor_factory": actor_factory})
def register_actor_sync(self, actor_type, actor_factory=None):
# Synchronous version for Flask
self.registrations.append({"actor_type": actor_type, "actor_factory": actor_factory})
class MockFlaskDaprActor:
"""Mock Flask actor with synchronous register_actor method."""
def __init__(self):
self.registrations = []
def register_actor(self, actor_type, actor_factory=None):
self.registrations.append({"actor_type": actor_type, "actor_factory": actor_factory})
@pytest.fixture
def mock_kernel() -> MagicMock:
"""Provides a mock kernel object."""
return MagicMock(name="Kernel")
@pytest.fixture
def mock_factories() -> dict:
"""Provides a mock factories dictionary."""
return {"mock": lambda: "mock_factory"}
def test_create_actor_factories_returns_factories(mock_kernel, mock_factories):
"""
Test that create_actor_factories returns callable factory functions that construct actors
with the provided kernel and factories.
"""
process_factory, step_factory = create_actor_factories(mock_kernel, mock_factories)
# Check that the returned factories are callable
assert callable(process_factory)
assert callable(step_factory)
# Create mock context and actor_id
mock_ctx = MagicMock()
mock_actor_id = MagicMock()
mock_actor_id.id = "actor_1"
# Call the factories to create ProcessActor and StepActor objects
process_actor = process_factory(mock_ctx, mock_actor_id)
step_actor = step_factory(mock_ctx, mock_actor_id)
# Check that the actors have the kernel and factories set correctly
assert hasattr(process_actor, "kernel")
assert process_actor.kernel == mock_kernel
assert hasattr(process_actor, "factories")
assert process_actor.factories == mock_factories
assert hasattr(step_actor, "kernel")
assert step_actor.kernel == mock_kernel
assert hasattr(step_actor, "factories")
assert step_actor.factories == mock_factories
async def test_register_fastapi_dapr_actors(mock_kernel, mock_factories):
"""
Test that register_fastapi_dapr_actors registers all the required actors with appropriate
factories for FastAPI.
"""
mock_actor = MockActor()
# Call the registration function
await register_fastapi_dapr_actors(mock_actor, mock_kernel, mock_factories)
# There should be 5 registrations: ProcessActor, StepActor (with factories) and
# three registrations without factories
expected_actor_types = {ProcessActor, StepActor, EventBufferActor, MessageBufferActor, ExternalEventBufferActor}
registered_actor_types = {reg["actor_type"] for reg in mock_actor.registrations}
assert expected_actor_types == registered_actor_types
# Verify that ProcessActor and StepActor registrations have an actor_factory
for reg in mock_actor.registrations:
if reg["actor_type"] in {ProcessActor, StepActor}:
assert reg["actor_factory"] is not None
else:
assert reg.get("actor_factory") is None
def test_register_flask_dapr_actors(mock_kernel, mock_factories):
"""Test that register_flask_dapr_actors registers all the required actors with appropriate factories for Flask."""
mock_actor = MockFlaskDaprActor()
# Call the synchronous registration function
register_flask_dapr_actors(mock_actor, mock_kernel, mock_factories)
# There should be 5 registrations: ProcessActor, StepActor (with factories) and
# three registrations without factories
expected_actor_types = {ProcessActor, StepActor, EventBufferActor, MessageBufferActor, ExternalEventBufferActor}
registered_actor_types = {reg["actor_type"] for reg in mock_actor.registrations}
assert expected_actor_types == registered_actor_types
# Check that ProcessActor and StepActor registrations have non-null actor_factory
for reg in mock_actor.registrations:
if reg["actor_type"] in {ProcessActor, StepActor}:
assert reg["actor_factory"] is not None
else:
assert reg.get("actor_factory") is None
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.dapr_runtime.dapr_kernel_process import start
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
class FakeDaprKernelProcessContext:
def __init__(self, process, max_supersteps: int | None = None):
self.process = process
self.start_with_event = AsyncMock()
self.max_supersteps = max_supersteps
async def test_start_with_valid_parameters():
state = MagicMock(spec=KernelProcessState)
state.name = "valid_state"
state.id = "state_1"
mock_step = MagicMock(spec=KernelProcessStepInfo)
process = KernelProcess(state=state, steps=[mock_step])
kernel = MagicMock(spec=Kernel)
initial_event = KernelProcessEvent(id="event_1", data="data_1")
with patch(
"semantic_kernel.processes.dapr_runtime.dapr_kernel_process.DaprKernelProcessContext",
new=FakeDaprKernelProcessContext,
):
result = await start(process=process, kernel=kernel, initial_event=initial_event, max_supersteps=10)
assert isinstance(result, FakeDaprKernelProcessContext)
assert result.process == process
assert result.max_supersteps == 10
result.start_with_event.assert_called_once_with(initial_event)
async def test_start_with_invalid_process():
kernel = MagicMock(spec=Kernel)
initial_event = KernelProcessEvent(id="event_1", data="data_1")
with pytest.raises(ProcessInvalidConfigurationException, match="process cannot be None"):
await start(process=None, kernel=kernel, initial_event=initial_event)
async def test_start_with_invalid_initial_event():
state = MagicMock(spec=KernelProcessState)
type(state).name = PropertyMock(return_value="valid_state")
mock_step = MagicMock(spec=KernelProcessStepInfo)
process = KernelProcess(state=state, steps=[mock_step])
kernel = MagicMock(spec=Kernel)
with pytest.raises(ProcessInvalidConfigurationException, match="initial_event cannot be None"):
await start(process=process, kernel=kernel, initial_event=None)
async def test_start_with_initial_event_as_string():
state = MagicMock(spec=KernelProcessState)
type(state).name = PropertyMock(return_value="valid_state")
mock_step = MagicMock(spec=KernelProcessStepInfo)
process = KernelProcess(state=state, steps=[mock_step])
kernel = MagicMock(spec=Kernel)
initial_event = "start_event"
with patch(
"semantic_kernel.processes.dapr_runtime.dapr_kernel_process.DaprKernelProcessContext", new=MagicMock()
) as mock_process_context_class:
mock_process_context_instance = mock_process_context_class.return_value
mock_process_context_instance.start_with_event = AsyncMock()
result = await start(process=process, kernel=kernel, initial_event=initial_event, data="event_data")
assert result == mock_process_context_instance
mock_process_context_instance.start_with_event.assert_called_once()
args, _ = mock_process_context_instance.start_with_event.call_args
actual_event = args[0]
assert isinstance(actual_event, KernelProcessEvent)
assert actual_event.id == "start_event"
assert actual_event.data == "event_data"
@@ -0,0 +1,101 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
from unittest.mock import AsyncMock, patch
import pytest
from semantic_kernel.processes.dapr_runtime.dapr_kernel_process_context import DaprKernelProcessContext
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.interfaces.process_interface import ProcessInterface
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 import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
class DummyInnerStepType(KernelProcessStep):
"""A valid KernelProcessStep subclass for testing."""
pass
@pytest.fixture
def process_context():
state = KernelProcessState(name="TestProcess", version="1.0", id=str(uuid.uuid4()))
step_state = KernelProcessStepState(name="TestStep", version="1.0", id="step1")
step = KernelProcessStepInfo(
state=step_state,
inner_step_type=DummyInnerStepType,
output_edges={},
)
steps = [step]
process = KernelProcess(state=state, steps=steps)
with patch("dapr.actor.ActorProxy.create") as mock_actor_proxy_create:
mock_dapr_process = AsyncMock(spec=ProcessInterface)
mock_actor_proxy_create.return_value = mock_dapr_process
context = DaprKernelProcessContext(
process=process,
allowed_module_prefixes=("semantic_kernel.", DummyInnerStepType.__module__),
)
yield context, mock_dapr_process
async def test_start_with_event(process_context):
context, mock_dapr_process = process_context
initial_event = KernelProcessEvent(id="event1", data="some data")
await context.start_with_event(initial_event)
dapr_process_info = DaprProcessInfo.from_kernel_process(context.process)
expected_payload = {
"process_info": dapr_process_info.model_dump_json(),
"parent_process_id": None,
"max_supersteps": context.max_supersteps,
}
mock_dapr_process.initialize_process.assert_awaited_once_with(expected_payload)
initial_event_json = initial_event.model_dump_json()
mock_dapr_process.run_once.assert_awaited_once_with(initial_event_json)
async def test_send_event(process_context):
context, mock_dapr_process = process_context
event = KernelProcessEvent(id="event2", data="event data")
await context.send_event(event)
mock_dapr_process.send_message.assert_awaited_once_with(event)
async def test_stop(process_context):
context, mock_dapr_process = process_context
await context.stop()
mock_dapr_process.stop.assert_awaited_once()
async def test_get_state(process_context):
context, mock_dapr_process = process_context
dapr_process_info = DaprProcessInfo.from_kernel_process(context.process)
mock_dapr_process.get_process_info.return_value = dapr_process_info
result = await context.get_state()
mock_dapr_process.get_process_info.assert_awaited_once()
assert isinstance(result, KernelProcess)
assert result.state.id == context.process.state.id
assert result.state.name == context.process.state.name
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from unittest.mock import AsyncMock
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.serializers import DefaultJSONSerializer
from semantic_kernel.processes.dapr_runtime.actors.event_buffer_actor import EventBufferActor
def _run(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(EventBufferActor)
message_serializer = DefaultJSONSerializer()
state_serializer = DefaultJSONSerializer()
mock_actor_client = AsyncMock()
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=message_serializer,
state_serializer=state_serializer,
actor_client=mock_actor_client,
)
actor = EventBufferActor(runtime_context, actor_id)
_run(actor._on_activate())
return actor
def test_enqueue(actor_context):
_run(actor_context.enqueue('{"event": "test_event"}'))
queue_list = list(actor_context.queue.queue)
assert queue_list == ['{"event": "test_event"}']
def test_dequeue_all(actor_context):
_run(actor_context.enqueue('{"event": "event1"}'))
_run(actor_context.enqueue('{"event": "event2"}'))
messages = _run(actor_context.dequeue_all())
assert messages == ['{"event": "event1"}', '{"event": "event2"}']
assert actor_context.queue.empty()
def test_on_activate(actor_context):
new_actor = EventBufferActor(actor_context.runtime_ctx, actor_context.id)
_run(new_actor._on_activate())
assert new_actor.queue.empty()
@@ -0,0 +1,100 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from unittest.mock import AsyncMock
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.serializers import DefaultJSONSerializer
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.external_event_buffer_actor import ExternalEventBufferActor
def _run(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(ExternalEventBufferActor)
message_serializer = DefaultJSONSerializer()
state_serializer = DefaultJSONSerializer()
mock_actor_client = AsyncMock()
mock_state_manager = AsyncMock()
mock_state_manager.try_get_state.return_value = (True, json.dumps([]))
mock_state_manager.try_add_state = AsyncMock()
mock_state_manager.save_state = AsyncMock()
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=message_serializer,
state_serializer=state_serializer,
actor_client=mock_actor_client,
)
actor = ExternalEventBufferActor(runtime_context, actor_id)
actor._state_manager = mock_state_manager
_run(actor._on_activate())
return actor
def test_enqueue(actor_context):
_run(actor_context.enqueue('{"event": "test_event"}'))
queue_list = list(actor_context.queue.queue)
assert queue_list == ['{"event": "test_event"}']
actor_context._state_manager.try_add_state.assert_called_with(
ActorStateKeys.ExternalEventQueueState.value, json.dumps(['{"event": "test_event"}'])
)
actor_context._state_manager.save_state.assert_called_once()
def test_dequeue_all(actor_context):
_run(actor_context.enqueue('{"event": "event1"}'))
_run(actor_context.enqueue('{"event": "event2"}'))
messages = _run(actor_context.dequeue_all())
assert messages == ['{"event": "event1"}', '{"event": "event2"}']
assert actor_context.queue.empty()
actor_context._state_manager.try_add_state.assert_called_with(
ActorStateKeys.ExternalEventQueueState.value, json.dumps([])
)
actor_context._state_manager.save_state.assert_called()
def test_on_activate_with_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (
True,
json.dumps(['{"event": "event1"}', '{"event": "event2"}']),
)
new_actor = ExternalEventBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
queue_list = list(new_actor.queue.queue)
assert queue_list == ['{"event": "event1"}', '{"event": "event2"}']
def test_on_activate_with_no_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (False, None)
new_actor = ExternalEventBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
assert new_actor.queue.empty()
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from unittest.mock import AsyncMock
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.serializers import DefaultJSONSerializer
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.message_buffer_actor import MessageBufferActor
def _run(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(MessageBufferActor)
message_serializer = DefaultJSONSerializer()
state_serializer = DefaultJSONSerializer()
mock_actor_client = AsyncMock()
mock_state_manager = AsyncMock()
mock_state_manager.try_get_state.return_value = (True, [])
mock_state_manager.try_add_state = AsyncMock()
mock_state_manager.save_state = AsyncMock()
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=message_serializer,
state_serializer=state_serializer,
actor_client=mock_actor_client,
)
actor = MessageBufferActor(runtime_context, actor_id)
actor._state_manager = mock_state_manager
_run(actor._on_activate())
return actor
def test_enqueue(actor_context):
_run(actor_context.enqueue('{"event": "test_event"}'))
queue_list = list(actor_context.queue.queue)
assert queue_list == ['{"event": "test_event"}']
actor_context._state_manager.try_add_state.assert_called_with(
ActorStateKeys.MessageQueueState.value, ['{"event": "test_event"}']
)
actor_context._state_manager.save_state.assert_called_once()
def test_dequeue_all(actor_context):
_run(actor_context.enqueue('{"event": "event1"}'))
_run(actor_context.enqueue('{"event": "event2"}'))
actor_context._state_manager.reset_mock()
messages = _run(actor_context.dequeue_all())
assert messages == ['{"event": "event1"}', '{"event": "event2"}']
assert actor_context.queue.empty()
actor_context._state_manager.try_add_state.assert_called_with(ActorStateKeys.MessageQueueState.value, "[]")
actor_context._state_manager.save_state.assert_called_once()
def test_on_activate_with_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (True, ['{"event": "event1"}', '{"event": "event2"}'])
new_actor = MessageBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
queue_list = list(new_actor.queue.queue)
assert queue_list == ['{"event": "event1"}', '{"event": "event2"}']
def test_on_activate_with_no_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (False, None)
new_actor = MessageBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
assert new_actor.queue.empty()
@@ -0,0 +1,283 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from queue import Queue
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.exceptions.process_exceptions import ProcessEventUndefinedException
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.process_actor import ProcessActor
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(ProcessActor)
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=MagicMock(),
state_serializer=MagicMock(),
actor_client=MagicMock(),
)
kernel_mock = MagicMock()
actor = ProcessActor(runtime_context, actor_id, kernel=kernel_mock, factories={})
actor._state_manager = AsyncMock()
actor._state_manager.try_add_state = AsyncMock(return_value=True)
actor._state_manager.try_get_state = AsyncMock(return_value=(True, {}))
actor._state_manager.save_state = AsyncMock()
asyncio.run(actor._on_activate())
return actor
def clean_structure(data):
"""Recursively remove null values and empty dictionaries for direct comparison."""
if isinstance(data, dict):
return {k: clean_structure(v) for k, v in data.items() if v not in [None, {}]}
if isinstance(data, list):
return [clean_structure(item) for item in data]
return data
async def test_initialize_process(actor_context):
input_data = {
"process_info": {
"type": "DaprProcessInfo",
"state": {"type": "KernelProcessState", "name": "Test Process", "version": "1.0", "id": "proc_123"},
"steps": [
{
"type": "DaprStepInfo",
"inner_step_python_type": "SomeStepType",
"state": {"type": "KernelProcessState", "name": "Step1", "version": "1.0", "id": "step_123"},
}
],
"inner_step_python_type": "SomeProcessType",
"edges": {},
},
"parent_process_id": "parent_123",
}
expected_process_info = clean_structure(input_data["process_info"])
dapr_process_info_instance = DaprProcessInfo(
inner_step_python_type="SomeProcessType",
state=KernelProcessState(name="Test Process", version="1.0", id="proc_123"),
edges={},
steps=[
DaprStepInfo(
inner_step_python_type="SomeStepType",
state=KernelProcessState(name="Step1", version="1.0", id="step_123"),
edges={},
)
],
)
with (
patch.multiple(
actor_context,
_initialize_process_actor=AsyncMock(),
_state_manager=actor_context._state_manager,
),
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
):
await actor_context.initialize_process(input_data)
actual_calls = actor_context._state_manager.try_add_state.call_args_list
actual_process_info_call = next(
(call for call in actual_calls if call[0][0] == ActorStateKeys.ProcessInfoState.value), None
)
assert actual_process_info_call is not None, "ProcessInfoState call was not found."
actual_process_info = clean_structure(actual_process_info_call[0][1])
assert actual_process_info == expected_process_info, (
f"Expected: {json.dumps(expected_process_info)}, but got: {json.dumps(actual_process_info)}"
)
mock_save_state.assert_called_once()
actor_context._initialize_process_actor.assert_called_once_with(dapr_process_info_instance, "parent_123")
async def test_start_process(actor_context):
actor_context.initialize_task = True
with patch.object(actor_context, "internal_execute", new=AsyncMock()) as mock_internal_execute:
await actor_context.start(keep_alive=False)
assert actor_context.process_task is not None
mock_internal_execute.assert_called_once()
assert not actor_context.process_task.done()
def test_run_once(actor_context):
actor_context.initialize_task = True
process_event = '{"event": "test_event"}'
with patch(
"semantic_kernel.processes.dapr_runtime.actors.process_actor.ActorProxy.create", return_value=AsyncMock()
) as mock_proxy:
asyncio.run(actor_context.run_once(process_event))
external_event_queue = mock_proxy.return_value
external_event_queue.enqueue.assert_called_once_with(process_event)
assert actor_context.process_task is not None
async def test_stop(actor_context):
actor_context.initialize_task = True
actor_context.process_task = asyncio.create_task(asyncio.sleep(1))
await actor_context.stop()
assert actor_context.process_task.done()
def test_get_process_info(actor_context):
with patch.object(actor_context, "to_dapr_process_info", return_value=MagicMock()) as mock_to_dapr_process_info:
process_info = asyncio.run(actor_context.get_process_info())
mock_to_dapr_process_info.assert_called_once()
assert process_info is mock_to_dapr_process_info.return_value
def test_handle_message(actor_context):
message_mock = MagicMock()
message_mock.target_event_id = "test_event"
actor_context.output_edges = {"test_event": [MagicMock()]}
with patch.object(actor_context, "run_once", new=AsyncMock()) as mock_run_once:
asyncio.run(actor_context.handle_message(message_mock))
mock_run_once.assert_called_once()
@pytest.fixture
def actor() -> ProcessActor:
"""Create a fresh ProcessActor with mocked dependencies."""
# Arrange: Create a dummy runtime context and ProcessActor instance
actor_id = ActorId("test_actor")
runtime_context = MagicMock()
kernel = MagicMock()
actor_obj = ProcessActor(runtime_context, actor_id, kernel=kernel, factories={})
# Mock internal state manager
actor_obj._state_manager = AsyncMock()
actor_obj._state_manager.try_get_state = AsyncMock(return_value=(False, None))
actor_obj._state_manager.try_add_state = AsyncMock()
actor_obj._state_manager.save_state = AsyncMock()
return actor_obj
def test_name_uninitialized(actor: ProcessActor):
"""Test that accessing name before initialization raises KernelException."""
with pytest.raises(KernelException) as exc_info:
_ = actor.name
assert "must be initialized before accessing the name" in str(exc_info.value)
@pytest.mark.asyncio
async def test_start_not_initialized(actor: ProcessActor):
"""Test that start() without initialization raises ValueError."""
actor.initialize_task = False
with pytest.raises(ValueError):
await actor.start()
@pytest.mark.asyncio
async def test_run_once_none_event(actor: ProcessActor):
"""Test that run_once(None) raises ProcessEventUndefinedException."""
actor.initialize_task = True
with pytest.raises(ProcessEventUndefinedException):
await actor.run_once(None) # type: ignore
@pytest.mark.asyncio
async def test_send_message_none_event(actor: ProcessActor):
"""Test that send_message(None) raises ProcessEventUndefinedException."""
with pytest.raises(ProcessEventUndefinedException):
await actor.send_message(None) # type: ignore
def test_send_message_success(actor: ProcessActor):
"""Test that send_message enqueues the event into external_event_queue."""
event = KernelProcessEvent(id="e1", data="d1")
asyncio.run(actor.send_message(event))
assert isinstance(actor.external_event_queue, Queue)
assert not actor.external_event_queue.empty()
queued = actor.external_event_queue.get()
assert queued is event
@pytest.mark.asyncio
async def test_to_dapr_process_info_uninitialized(actor: ProcessActor):
"""Test to_dapr_process_info raises ValueError if process is None."""
actor.process = None
with pytest.raises(ValueError) as exc:
await actor.to_dapr_process_info()
assert "must be initialized before converting" in str(exc.value)
@pytest.mark.asyncio
async def test_to_dapr_process_info_inner_step_type_none(actor: ProcessActor):
"""Test to_dapr_process_info raises ValueError if inner_step_python_type is None."""
actor.process = MagicMock()
# Simulate a process with missing inner_step_python_type
actor.process.inner_step_python_type = None
actor.process.state = MagicMock(name="Proc", id="pid")
actor.steps = []
with pytest.raises(ValueError) as exc:
await actor.to_dapr_process_info()
assert "inner step type must be defined" in str(exc.value)
@pytest.mark.asyncio
async def test_to_dapr_process_info_success(actor: ProcessActor):
"""Test to_dapr_process_info returns correct dict for initialized process with no steps."""
proc_state = KernelProcessState(name="Proc", version="1.0", id="test_actor")
dapr_proc = DaprProcessInfo(
inner_step_python_type="Type1",
state=proc_state,
edges={},
steps=[],
)
actor.process = dapr_proc
actor.steps = []
result = await actor.to_dapr_process_info()
assert result == dapr_proc.model_dump()
@pytest.mark.asyncio
async def test_stop_no_task(actor: ProcessActor):
"""Test stop() returns normally when no process_task is running."""
actor.process_task = None
await actor.stop()
def test_name_after_manual_set(actor: ProcessActor):
"""Test that name property returns the correct name after manual initialization."""
actor.process = MagicMock()
actor.process.state = MagicMock()
actor.process.state.name = "MyProcess"
actor.process.state.id = "id123"
assert actor.name == "MyProcess"
@pytest.mark.asyncio
async def test_send_outgoing_public_events_no_parent(actor: ProcessActor):
"""Test send_outgoing_public_events does nothing if parent_process_id is None."""
actor.parent_process_id = None
with patch("semantic_kernel.processes.dapr_runtime.actors.process_actor.ActorProxy.create") as mock_proxy:
await actor.send_outgoing_public_events()
mock_proxy.assert_not_called()
@@ -0,0 +1,465 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from typing import cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from dapr.actor import ActorId, ActorProxy
from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.exceptions.process_exceptions import (
ProcessFunctionNotFoundException,
)
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.event_buffer_actor import EventBufferActor
from semantic_kernel.processes.dapr_runtime.actors.step_actor import StepActor
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
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_function_target import KernelProcessFunctionTarget
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.process_event import ProcessEvent
from semantic_kernel.processes.process_message import ProcessMessage
class FakeStep:
async def activate(self, state):
self.activated_state = state
class FakeState:
pass
@pytest.fixture
def actor_context():
ctx = MagicMock()
actor_id = ActorId("test_actor")
kernel = MagicMock()
return StepActor(ctx, actor_id, kernel, factories={})
async def test_initialize_step(actor_context):
input_data = json.dumps({
"step_info": {
"state": {"name": "TestStep", "version": "1.0", "id": "step_1"},
"inner_step_python_type": "SomeStepType",
"edges": {},
},
"parent_process_id": "parent_1",
})
with (
patch.object(actor_context._state_manager, "try_add_state", new=AsyncMock()) as mock_try_add_state,
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
):
await actor_context.initialize_step(input_data)
assert actor_context.step_info is not None
mock_try_add_state.assert_any_call("DaprStepInfo", actor_context.step_info.model_dump_json())
mock_save_state.assert_called_once()
async def test_prepare_incoming_messages(actor_context):
message = ProcessMessage(
source_id="source_1",
destination_id="dest_1",
function_name="test_function",
values={"param1": "value1"},
)
mock_message_json = json.dumps(message.model_dump())
expected_state_key = "incomingMessagesState"
with (
patch("dapr.actor.ActorProxy.create", new=MagicMock()) as mock_actor_proxy,
patch.object(actor_context._state_manager, "try_add_state", new=AsyncMock()) as mock_try_add_state,
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
):
mock_queue = AsyncMock()
mock_queue.dequeue_all.return_value = [mock_message_json]
mock_actor_proxy.return_value = mock_queue
incoming_message_count = await actor_context.prepare_incoming_messages()
assert incoming_message_count == 1
assert actor_context.incoming_messages.qsize() == 1
mock_try_add_state.assert_called_with(expected_state_key, [mock_message_json])
mock_save_state.assert_called_once()
async def test_process_incoming_messages(actor_context):
actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="Test Step", version="1.0", id="step_123"),
inner_step_python_type="SomeStepType",
edges={},
)
message = ProcessMessage(
source_id="source_1",
destination_id="dest_1",
function_name="test_function",
values={"param1": "value1"},
)
actor_context.incoming_messages.put(message)
with (
patch.object(actor_context, "handle_message", new=AsyncMock()) as mock_handle_message,
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
patch.object(
actor_context._state_manager, "try_add_state", new=AsyncMock(return_value=True)
) as mock_try_add_state,
):
await actor_context.process_incoming_messages()
mock_handle_message.assert_called_once_with(message)
mock_save_state.assert_called_once()
expected_messages = []
expected_messages = [json.dumps(msg.model_dump()) for msg in list(actor_context.incoming_messages.queue)]
mock_try_add_state.assert_any_call("incomingMessagesState", expected_messages)
async def test_activate_step_with_factory_creates_state(actor_context):
fake_step_instance = FakeStep()
fake_step_instance.activate = AsyncMock(side_effect=fake_step_instance.activate)
fake_plugin = MagicMock()
fake_plugin.functions = {"test_function": lambda x: x}
with (
patch(
"semantic_kernel.processes.dapr_runtime.actors.step_actor.get_generic_state_type",
return_value=FakeState,
),
patch(
"semantic_kernel.processes.dapr_runtime.actors.step_actor.get_fully_qualified_name",
return_value="FakeStateFullyQualified",
),
patch(
"semantic_kernel.processes.dapr_runtime.actors.step_actor.find_input_channels",
return_value={"channel": {"input": "value"}},
),
):
actor_context.factories = {"FakeStep": lambda: fake_step_instance}
actor_context.inner_step_type = "FakeStep"
actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="default_name", version="2.0", id="step_123"),
inner_step_python_type="FakeStep",
edges={},
)
actor_context.kernel.add_plugin = MagicMock(return_value=fake_plugin)
actor_context._state_manager.try_add_state = AsyncMock()
actor_context._state_manager.save_state = AsyncMock()
await actor_context.activate_step()
actor_context.kernel.add_plugin.assert_called_once_with(fake_step_instance, "default_name")
assert actor_context.functions == fake_plugin.functions
assert actor_context.initial_inputs == {"channel": {"input": "value"}}
assert actor_context.inputs == {"channel": {"input": "value"}}
assert actor_context.step_state is not None
assert actor_context.step_state.version == "2.0"
assert isinstance(actor_context.step_state.state, FakeState)
fake_step_instance.activate.assert_awaited_once_with(actor_context.step_state)
async def test_activate_step_with_factory_uses_existing_state(actor_context):
fake_step_instance = FakeStep()
fake_step_instance.activate = AsyncMock(side_effect=fake_step_instance.activate)
fake_plugin = MagicMock()
fake_plugin.functions = {"test_function": lambda x: x}
pre_existing_state = KernelProcessStepState(name="ExistingState", version="v1.0", id="ExistingState", state=None)
with (
patch.object(
KernelProcessStepState,
"model_dump",
return_value={"name": "ExistingState", "id": "ExistingState", "state": None},
),
patch(
"semantic_kernel.processes.dapr_runtime.actors.step_actor.get_generic_state_type",
return_value=FakeState,
),
patch(
"semantic_kernel.processes.dapr_runtime.actors.step_actor.get_fully_qualified_name",
return_value="FakeStateFullyQualified",
),
patch(
"semantic_kernel.processes.dapr_runtime.actors.step_actor.find_input_channels",
return_value={"channel": {"input": "value"}},
),
):
actor_context.factories = {"FakeStep": lambda: fake_step_instance}
actor_context.inner_step_type = "FakeStep"
actor_context.step_info = DaprStepInfo(state=pre_existing_state, inner_step_python_type="FakeStep", edges={})
actor_context.kernel.add_plugin = MagicMock(return_value=fake_plugin)
actor_context._state_manager.try_add_state = AsyncMock()
actor_context._state_manager.save_state = AsyncMock()
await actor_context.activate_step()
actor_context.kernel.add_plugin.assert_called_once_with(fake_step_instance, pre_existing_state.name)
assert actor_context.functions == fake_plugin.functions
assert actor_context.initial_inputs == {"channel": {"input": "value"}}
assert actor_context.inputs == {"channel": {"input": "value"}}
assert actor_context.step_state is not None
assert actor_context.step_state.version == "v1.0"
actor_context._state_manager.try_add_state.assert_any_await(
ActorStateKeys.StepStateType.value, "FakeStateFullyQualified"
)
actor_context._state_manager.try_add_state.assert_any_await(
ActorStateKeys.StepStateJson.value, json.dumps(pre_existing_state.model_dump())
)
actor_context._state_manager.save_state.assert_awaited_once()
assert isinstance(actor_context.step_state.state, FakeState)
fake_step_instance.activate.assert_awaited_once_with(actor_context.step_state)
@pytest.fixture
def mock_actor_context():
"""Provides a fresh StepActor instance with mocked dependencies."""
ctx = MagicMock()
actor_id = ActorId("test_actor")
kernel = MagicMock()
actor = StepActor(ctx, actor_id, kernel, factories={})
actor._state_manager = MagicMock()
return actor
def test_name_property_before_init_raises(mock_actor_context):
"""Accessing .name before initialization raises KernelException"""
with pytest.raises(KernelException):
_ = mock_actor_context.name
async def test_name_property_after_init_returns_name(mock_actor_context):
"""After setting step_info, name returns the state's name"""
step_state = KernelProcessStepState(name="MyStep", version="v1", id="id1")
mock_actor_context.step_info = DaprStepInfo(
state=step_state,
inner_step_python_type="Any",
edges={},
)
# should return MyStep
assert mock_actor_context.name == "MyStep"
def test_get_edge_for_event_empty(mock_actor_context):
"""get_edge_for_event returns empty when no edges set"""
mock_actor_context.output_edges = {}
assert mock_actor_context.get_edge_for_event("event") == []
def test_get_edge_for_event_present(mock_actor_context):
"""get_edge_for_event returns correct list"""
edge = KernelProcessEdge(
source_step_id="s1",
output_target=KernelProcessFunctionTarget(
step_id="t1", function_name="f", parameter_name=None, target_event_id="e1"
),
)
mock_actor_context.output_edges = {"e1": [edge]}
assert mock_actor_context.get_edge_for_event("e1") == [edge]
def test_scoped_event_none_raises(mock_actor_context):
"""scoped_event with None raises ValueError"""
# Use cast to satisfy type checker
with pytest.raises(ValueError):
mock_actor_context.scoped_event(cast(ProcessEvent, None))
def test_scoped_event_sets_namespace(mock_actor_context):
"""scoped_event updates event namespace correctly"""
# prepare name and id
mock_actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="StepName", version="v1", id="id"),
inner_step_python_type="Any",
edges={},
)
# create event
evt = ProcessEvent(inner_event=KernelProcessEvent(id="e", data="d"), namespace=None)
result = mock_actor_context.scoped_event(evt)
assert result.namespace == f"StepName_{mock_actor_context.id.id}"
async def test_invoke_function_calls_kernel_invoke(mock_actor_context):
"""invoke_function delegates to kernel.invoke with correct args"""
fake_fn = MagicMock(name="f")
fake_kernel = MagicMock()
fake_kernel.invoke = AsyncMock(return_value="result")
res = await mock_actor_context.invoke_function(fake_fn, fake_kernel, {"a": 1})
fake_kernel.invoke.assert_awaited_once_with(fake_fn, a=1)
assert res == "result"
async def test_emit_event_without_namespace_raises(mock_actor_context):
"""emit_event without setting event_namespace raises ValueError"""
mock_actor_context.event_namespace = None
with pytest.raises(ValueError):
await mock_actor_context.emit_event(KernelProcessEvent(id="e", data="d"))
async def test_emit_process_event_public_and_edge(mock_actor_context):
"""emit_process_event enqueues event to parent buffer and sends messages to targets"""
# setup event as public and parent process id
mock_actor_context.parent_process_id = "parent1"
# fake ActorProxy.create to return AsyncMock for enqueue
mock_parent = AsyncMock(spec=EventBufferActor)
mock_child = AsyncMock(spec=EventBufferActor)
def fake_create(actor_type, actor_id, actor_interface):
# first call is for EventBufferInterface, second for MessageBufferInterface
if actor_interface.__name__ == "EventBufferInterface":
return mock_parent
return mock_child
with patch.object(ActorProxy, "create", side_effect=fake_create):
# create an edge for event
target = KernelProcessFunctionTarget(
step_id="child1", function_name="func", parameter_name="p", target_event_id="evt"
)
edge = KernelProcessEdge(source_step_id="src", output_target=target)
mock_actor_context.output_edges = {"evt": [edge]}
mock_actor_context.get_edge_for_event = MagicMock(return_value=[edge])
# call
evt = ProcessEvent(
inner_event=KernelProcessEvent(id="evt", data="data", visibility=KernelProcessEventVisibility.Public),
namespace="ns",
)
await mock_actor_context.emit_process_event(evt)
# verify enqueue on parent and child
mock_parent.enqueue.assert_awaited_once()
mock_child.enqueue.assert_awaited_once()
@patch.object(StepActor, "activate_step", AsyncMock())
async def test_to_dapr_step_info_errors(mock_actor_context):
"""to_dapr_step_info raises if uninitialized"""
# case: step_info not set
mock_actor_context.step_activated = False
mock_actor_context.step_info = None
with pytest.raises(ValueError):
await mock_actor_context.to_dapr_step_info()
# case: inner_step_type not set
mock_actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="S", version="v", id="id"),
inner_step_python_type="Type",
edges={},
)
mock_actor_context.inner_step_type = None
mock_actor_context.step_activated = True
with pytest.raises(ValueError):
await mock_actor_context.to_dapr_step_info()
async def test_to_dapr_step_info_success(mock_actor_context):
"""to_dapr_step_info returns correct model_dump"""
# setup valid step_info and inner_step_type
mock_actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="N", version="v", id="id"),
inner_step_python_type="Type",
edges={"e": []},
)
mock_actor_context.inner_step_type = "Type"
mock_actor_context.step_activated = True
result = await mock_actor_context.to_dapr_step_info()
# check returned dict structure
assert "inner_step_python_type" in result
assert result["inner_step_python_type"] == "Type"
assert result["edges"] == {"e": []}
async def test_handle_message_none_raises(mock_actor_context):
"""handle_message with None raises ValueError"""
with pytest.raises(ValueError):
await mock_actor_context.handle_message(None)
async def test_handle_message_no_function_found_raises(mock_actor_context):
"""handle_message raises when function exists in inputs but not in functions dict"""
# prepare actor state
mock_actor_context.step_activated = True
mock_actor_context.functions = {}
mock_actor_context.inputs = {"f1": {"a": "v"}}
mock_actor_context.initial_inputs = {"f1": {"a": "v"}}
mock_actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="S", version="v", id="id"),
inner_step_python_type="Type",
edges={},
)
from semantic_kernel.processes.process_message import ProcessMessage
msg = ProcessMessage(source_id="s", destination_id="d", function_name="f1", values={"a": "v"})
with pytest.raises(ProcessFunctionNotFoundException):
await mock_actor_context.handle_message(msg)
async def test_handle_message_function_returns_none_emits_error(mock_actor_context):
"""handle_message when invoke_function returns None emits an error event"""
mock_actor_context.step_activated = True
# ensure name property works by setting step_info
mock_actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="S", version="v", id="id"),
inner_step_python_type="Type",
edges={},
)
# prepare inputs and functions
fake_kernel_fn = MagicMock(plugin_name="plug", name="f1")
mock_actor_context.functions = {"f1": fake_kernel_fn}
mock_actor_context.inputs = {"f1": {"x": 1}}
mock_actor_context.initial_inputs = {"f1": {"x": None}}
# patch invoke_function to return None
mock_actor_context.invoke_function = AsyncMock(return_value=None)
# capture emit_event
mock_actor_context.emit_event = AsyncMock()
from semantic_kernel.processes.process_message import ProcessMessage
msg = ProcessMessage(source_id="s", destination_id="d", function_name="f1", values={"x": 1})
await mock_actor_context.handle_message(msg)
# should emit error event
emitted = mock_actor_context.emit_event.call_args.args[0]
assert isinstance(emitted, KernelProcessEvent)
assert emitted.id == "f1.OnError"
async def test_handle_message_success_emits_result_and_resets_inputs(mock_actor_context):
"""handle_message successfully invokes function and emits result event"""
# setup valid step_info and activation
mock_actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="S", version="v", id="id"),
inner_step_python_type="Type",
edges={},
)
mock_actor_context.step_activated = True
fake_kernel_fn = MagicMock(plugin_name="plug", name="f1")
mock_actor_context.functions = {"f1": fake_kernel_fn}
mock_actor_context.inputs = {"f1": {"x": 1}}
mock_actor_context.initial_inputs = {"f1": {"x": None}}
# define a dummy result class with a value attribute
class DummyResult:
def __init__(self, value):
self.value = value
r = DummyResult("out")
mock_actor_context.invoke_function = AsyncMock(return_value=r)
mock_actor_context.emit_event = AsyncMock()
from semantic_kernel.processes.process_message import ProcessMessage
msg = ProcessMessage(source_id="s", destination_id="d", function_name="f1", values={"x": 1})
await mock_actor_context.handle_message(msg)
# verify emitted result event and inputs reset
emitted = mock_actor_context.emit_event.call_args.args[0]
assert emitted.id == "f1.OnResult"
assert emitted.data == "out"
assert mock_actor_context.inputs["f1"] == {"x": None}
@@ -0,0 +1,199 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.step_utils import get_step_class_from_qualified_name
class MockValidStep(KernelProcessStep):
"""A valid KernelProcessStep subclass for testing."""
pass
class NotAStep:
"""A class that is NOT a KernelProcessStep subclass."""
pass
def test_valid_step_class_loads_successfully():
"""Test that a valid KernelProcessStep subclass loads correctly."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[MockValidStep.__module__],
)
assert result is MockValidStep
assert issubclass(result, KernelProcessStep)
def test_invalid_format_no_dots_raises_exception():
"""Test that a class name without dots raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name("NoDots")
def test_empty_module_name_raises_exception():
"""Test that empty module name (e.g., '.ClassName') raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Module name and class name cannot be empty"):
get_step_class_from_qualified_name(".ClassName")
def test_empty_class_name_raises_exception():
"""Test that empty class name (e.g., 'module.') raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Module name and class name cannot be empty"):
get_step_class_from_qualified_name("module.")
def test_empty_string_raises_exception():
"""Test that an empty string raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name("")
def test_none_like_empty_raises_exception():
"""Test that None-like values raise ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name(None) # type: ignore
def test_nonexistent_module_raises_exception():
"""Test that a non-existent module raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Unable to import module"):
get_step_class_from_qualified_name("nonexistent_module_xyz123.SomeClass", allowed_module_prefixes=None)
def test_nonexistent_class_in_valid_module_raises_exception():
"""Test that a non-existent class in a valid module raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="not found in module"):
get_step_class_from_qualified_name("semantic_kernel.kernel.NonExistentClass12345")
def test_non_class_attribute_raises_exception():
"""Test that loading a non-class attribute (function) raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not a class type"):
get_step_class_from_qualified_name("semantic_kernel.processes.step_utils.get_fully_qualified_name")
def test_non_step_class_raises_exception():
"""Test that a class not inheriting from KernelProcessStep raises exception."""
full_name = f"{NotAStep.__module__}.{NotAStep.__name__}"
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name(full_name, allowed_module_prefixes=[NotAStep.__module__])
def test_builtin_class_raises_exception():
"""Test that built-in classes like str raise exception (bypassing prefix check to test subclass validation)."""
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name("builtins.str", allowed_module_prefixes=None)
def test_os_system_prevented():
"""Test that os.system is prevented (bypassing prefix check to test type validation)."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not a class type"):
get_step_class_from_qualified_name("os.system", allowed_module_prefixes=None)
def test_arbitrary_class_prevented():
"""Test that arbitrary classes like subprocess.Popen are prevented (bypassing prefix check)."""
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name("subprocess.Popen", allowed_module_prefixes=None)
def test_kernel_class_prevented():
"""Test that even internal SK classes that aren't steps are prevented."""
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name("semantic_kernel.kernel.Kernel")
# Module allowlist tests
def test_allowlist_permits_valid_module():
"""Test that a valid step class from an allowed module loads successfully."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
# Allow the test module (use actual module name from __module__)
module_prefix = MockValidStep.__module__
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[module_prefix],
)
assert result is MockValidStep
def test_allowlist_blocks_disallowed_module():
"""Test that a module not in the allowlist is blocked BEFORE import."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name(
"os.path",
allowed_module_prefixes=["semantic_kernel.", "myapp."],
)
def test_allowlist_blocks_dangerous_module():
"""Test that dangerous modules are blocked when allowlist is provided."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name(
"subprocess.Popen",
allowed_module_prefixes=["semantic_kernel."],
)
def test_empty_allowlist_blocks_all():
"""Test that an empty allowlist blocks all modules."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name(full_name, allowed_module_prefixes=[])
def test_none_allowlist_allows_all():
"""Test that None allowlist allows any module (explicit opt-out)."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
result = get_step_class_from_qualified_name(full_name, allowed_module_prefixes=None)
assert result is MockValidStep
def test_default_allowlist_blocks_non_sk_modules():
"""Test that the default allowlist only permits semantic_kernel modules."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name("subprocess.Popen")
def test_default_allowlist_permits_sk_modules():
"""Test that the default allowlist permits semantic_kernel modules."""
full_name = "semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep"
result = get_step_class_from_qualified_name(full_name)
assert result is KernelProcessStep
def test_allowlist_prefix_matching():
"""Test that allowlist uses boundary-aware prefix matching correctly."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
# Exact module name match works
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[MockValidStep.__module__],
)
assert result is MockValidStep
# Arbitrary substring prefix without dot boundary does not match
short_prefix = MockValidStep.__module__[:4] # e.g. "test"
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[short_prefix],
)
def test_allowlist_multiple_prefixes():
"""Test that multiple allowed prefixes work correctly."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
module_prefix = MockValidStep.__module__
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=["semantic_kernel.", "myapp.", module_prefix],
)
assert result is MockValidStep
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
import pytest
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_state import KernelProcessState
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
def test_initialization_with_valid_parameters():
# Arrange
state = MagicMock(spec=KernelProcessState)
state.name = "valid_state"
steps = [MagicMock(spec=KernelProcessStepInfo)]
edges = {"step1": [MagicMock(spec=KernelProcessEdge)]}
# Act
process = KernelProcess(state=state, steps=steps, edges=edges)
# Assert
assert process.state == state
assert process.steps == steps
assert process.output_edges == edges
def test_initialization_with_no_steps():
# Arrange
state = MagicMock(spec=KernelProcessState)
state.name = "state_without_steps"
# Act & Assert
with pytest.raises(ValueError, match="steps cannot be None"):
KernelProcess(state=state, steps=[])
def test_initialization_with_no_state():
# Arrange
steps = [MagicMock(spec=KernelProcessStepInfo)]
edges = {"step1": [MagicMock(spec=KernelProcessEdge)]}
# Act & Assert
with pytest.raises(ValueError, match="state cannot be None"):
KernelProcess(state=None, steps=steps, edges=edges)
def test_initialization_with_no_state_name():
# Arrange
state = MagicMock(spec=KernelProcessState)
state.name = None # Invalid state name
steps = [MagicMock(spec=KernelProcessStepInfo)]
# Act & Assert
with pytest.raises(ValueError, match="state.Name cannot be None"):
KernelProcess(state=state, steps=steps)
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
def test_initialization():
"""Test initialization of KernelProcessEdge with valid input."""
source_step_id = "step_1"
output_target = KernelProcessFunctionTarget(
step_id="step_2", function_name="process_data", parameter_name="input_data", target_event_id="event_1"
)
edge = KernelProcessEdge(source_step_id=source_step_id, output_target=output_target)
assert edge.source_step_id == source_step_id
assert edge.output_target == output_target
def test_initialization_with_none_parameter_name():
"""Test KernelProcessEdge initialization with a target having None as parameter_name."""
source_step_id = "step_1"
output_target = KernelProcessFunctionTarget(
step_id="step_2", function_name="process_data", parameter_name=None, target_event_id="event_1"
)
edge = KernelProcessEdge(source_step_id=source_step_id, output_target=output_target)
assert edge.source_step_id == source_step_id
assert edge.output_target.parameter_name is None
def test_initialization_with_none_target_event_id():
"""Test KernelProcessEdge initialization with a target having None as target_event_id."""
source_step_id = "step_1"
output_target = KernelProcessFunctionTarget(
step_id="step_2", function_name="process_data", parameter_name="input_data", target_event_id=None
)
edge = KernelProcessEdge(source_step_id=source_step_id, output_target=output_target)
assert edge.source_step_id == source_step_id
assert edge.output_target.target_event_id is None
def test_initialization_with_empty_strings():
"""Test KernelProcessEdge initialization with empty strings for step_id and function_name."""
source_step_id = ""
output_target = KernelProcessFunctionTarget(step_id="", function_name="", parameter_name="", target_event_id="")
edge = KernelProcessEdge(source_step_id=source_step_id, output_target=output_target)
assert edge.source_step_id == ""
assert edge.output_target.step_id == ""
assert edge.output_target.function_name == ""
assert edge.output_target.parameter_name == ""
assert edge.output_target.target_event_id == ""
@@ -0,0 +1,62 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.processes.kernel_process.kernel_process_event import (
KernelProcessEvent,
KernelProcessEventVisibility,
)
def test_initialization():
# Arrange
event_id = "event_001"
event_data = {"key": "value"}
# Act
event = KernelProcessEvent(id=event_id, data=event_data)
# Assert
assert event.id == event_id
assert event.data == event_data
assert event.visibility == KernelProcessEventVisibility.Internal
def test_initialization_with_visibility():
# Arrange
event_id = "event_002"
event_data = [1, 2, 3]
visibility = KernelProcessEventVisibility.Public
# Act
event = KernelProcessEvent(id=event_id, data=event_data, visibility=visibility)
# Assert
assert event.id == event_id
assert event.data == event_data
assert event.visibility == KernelProcessEventVisibility.Public
def test_invalid_visibility():
# Arrange
event_id = "event_003"
event_data = "sample data"
invalid_visibility = "Hidden"
# Act & Assert
with pytest.raises(ValueError):
KernelProcessEvent(id=event_id, data=event_data, visibility=invalid_visibility)
def test_none_data_initialization():
# Arrange
event_id = "event_004"
event_data = None
# Act
event = KernelProcessEvent(id=event_id, data=event_data)
# Assert
assert event.id == event_id
assert event.data is None
assert event.visibility == KernelProcessEventVisibility.Internal
@@ -0,0 +1,66 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import ValidationError
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
def test_initialization():
# Arrange
step_id = "step_001"
function_name = "execute_task"
parameter_name = "input_data"
target_event_id = "event_001"
# Act
target = KernelProcessFunctionTarget(
step_id=step_id, function_name=function_name, parameter_name=parameter_name, target_event_id=target_event_id
)
# Assert
assert target.step_id == step_id
assert target.function_name == function_name
assert target.parameter_name == parameter_name
assert target.target_event_id == target_event_id
def test_initialization_with_defaults():
# Arrange
step_id = "step_002"
function_name = "process_data"
# Act
target = KernelProcessFunctionTarget(step_id=step_id, function_name=function_name)
# Assert
assert target.step_id == step_id
assert target.function_name == function_name
assert target.parameter_name is None
assert target.target_event_id is None
def test_invalid_initialization():
# Arrange
step_id = 12345 # Invalid type
function_name = "compute"
# Act & Assert
with pytest.raises(ValidationError):
KernelProcessFunctionTarget(step_id=step_id, function_name=function_name)
def test_partial_initialization_with_parameter_name():
# Arrange
step_id = "step_003"
function_name = "calculate"
parameter_name = "value"
# Act
target = KernelProcessFunctionTarget(step_id=step_id, function_name=function_name, parameter_name=parameter_name)
# Assert
assert target.step_id == step_id
assert target.function_name == function_name
assert target.parameter_name == parameter_name
assert target.target_event_id is None
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import KernelProcessMessageChannel
from semantic_kernel.processes.local_runtime.local_event import KernelProcessEvent
class MockKernelProcessMessageChannel(KernelProcessMessageChannel):
async def emit_event(self, process_event: KernelProcessEvent) -> None:
pass
async def test_emit_event():
# Arrange
event = KernelProcessEvent(id="event_001", data={"key": "value"})
channel = MockKernelProcessMessageChannel()
channel.emit_event = AsyncMock()
# Act
await channel.emit_event(event)
# Assert
channel.emit_event.assert_awaited_once_with(event)
async def test_emit_event_with_no_event():
# Arrange
channel = MockKernelProcessMessageChannel()
channel.emit_event = AsyncMock()
# Act
await channel.emit_event(None)
# Assert
channel.emit_event.assert_awaited_once_with(None)
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import ValidationError
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
def test_initialization_with_name_and_id():
# Arrange
name = "test_process"
process_id = "1234"
version = "1.0"
# Act
state = KernelProcessState(name=name, version=version, id=process_id)
# Assert
assert state.name == name
assert state.id == process_id
assert state.state is None
assert state.version == version
def test_initialization_with_name_only():
# Arrange
name = "test_process_without_id"
version = "1.0"
# Act
state = KernelProcessState(name=name, version=version)
# Assert
assert state.name == name
assert state.id is None
assert state.state is None
assert state.version == version
def test_setting_state_value():
# Arrange
name = "test_process"
state_value = {"key": "value"}
# Act
state = KernelProcessState(name=name, version="1.0")
state.state = state_value
# Assert
assert state.state == state_value
assert state.version == "1.0"
def test_initialization_with_invalid_name():
# Arrange
name = 12345 # Invalid type for name
# Act & Assert
with pytest.raises(ValidationError):
KernelProcessState(name=name, version="1.0")
@@ -0,0 +1,86 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from pathlib import Path
from semantic_kernel.processes.kernel_process.kernel_process_step_state_metadata import (
KernelProcessStateMetadata,
KernelProcessStepStateMetadata,
)
def test_load_from_file_success(tmp_path: Path):
state_data = {"$type": "Process", "id": "123", "name": "TestProcess", "versionInfo": "v1", "stepsState": {}}
file_path = tmp_path / "test_state.json"
file_path.write_text(json.dumps(state_data), encoding="utf-8")
result = KernelProcessStateMetadata.load_from_file(
json_filename="test_state.json",
directory=tmp_path,
)
assert result is not None
assert result.id == "123"
assert result.name == "TestProcess"
assert result.version_info == "v1"
assert result.steps_state == {}
def test_load_from_file_file_not_found(tmp_path: Path):
result = KernelProcessStateMetadata.load_from_file(
json_filename="non_existing.json",
directory=tmp_path,
)
assert result is None
def test_load_from_file_invalid_json(tmp_path: Path):
file_path = tmp_path / "invalid.json"
file_path.write_text("not a valid json content", encoding="utf-8")
result = KernelProcessStateMetadata.load_from_file(
json_filename="invalid.json",
directory=tmp_path,
)
assert result is None
async def test_load_from_file_with_non_utf8_encoding(tmp_path: Path):
state_data = {
"$type": "Process",
"id": "456",
"name": "CaféProcess",
"versionInfo": "v2",
"stepsState": {},
}
file_path = tmp_path / "test_state_utf16.json"
file_path.write_text(json.dumps(state_data), encoding="utf-16")
# Try to read with wrong encoding (utf-8), we expect a parse error -> None
result_wrong_encoding = KernelProcessStateMetadata.load_from_file(
json_filename="test_state_utf16.json",
directory=tmp_path,
encoding="utf-8",
)
assert result_wrong_encoding is None
# Now read with correct encoding (utf-16), we expect a success
result_correct_encoding = KernelProcessStateMetadata.load_from_file(
json_filename="test_state_utf16.json",
directory=tmp_path,
encoding="utf-16",
)
assert result_correct_encoding is not None
assert result_correct_encoding.name == "CaféProcess"
def test_create_step_state_metadata():
step_state = KernelProcessStepStateMetadata(
type_="Step", id="step123", name="TestStep", version_info="v1", state={"foo": "bar"}
)
assert step_state.type_ == "Step"
assert step_state.id == "step123"
assert step_state.name == "TestStep"
assert step_state.version_info == "v1"
assert step_state.state == {"foo": "bar"}
@@ -0,0 +1,52 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessEventUndefinedException
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import KernelProcessMessageChannel
from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
from semantic_kernel.processes.local_runtime.local_event import KernelProcessEvent
class MockKernelProcessMessageChannel(KernelProcessMessageChannel):
async def emit_event(self, process_event: KernelProcessEvent) -> None:
pass
async def test_initialization():
# Arrange
channel = MockKernelProcessMessageChannel()
# Act
context = KernelProcessStepContext(channel=channel)
# Assert
assert context.step_message_channel == channel
async def test_emit_event():
# Arrange
channel = MockKernelProcessMessageChannel()
channel.emit_event = AsyncMock()
context = KernelProcessStepContext(channel=channel)
event = KernelProcessEvent(id="event_001", data={"key": "value"})
# Act
await context.emit_event(event)
# Assert
channel.emit_event.assert_called_once_with(event)
async def test_emit_event_with_invalid_event():
# Arrange
channel = MockKernelProcessMessageChannel()
channel.emit_event = AsyncMock()
context = KernelProcessStepContext(channel=channel)
# Act & Assert
with pytest.raises(ProcessEventUndefinedException):
await context.emit_event(None)
@@ -0,0 +1,68 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
import pytest
from pydantic import ValidationError
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
class MockStepType:
"""A mock class to simulate a step type."""
pass
def test_initialization():
# Arrange
inner_step_type = MockStepType
state = MagicMock(spec=KernelProcessStepState)
edge = MagicMock(spec=KernelProcessEdge)
edges = {"output": [edge]}
# Act
step_info = KernelProcessStepInfo(inner_step_type=inner_step_type, state=state, output_edges=edges)
# Assert
assert step_info.inner_step_type == inner_step_type
assert step_info.state == state
assert step_info.output_edges == edges
def test_initialization_with_missing_arguments():
# Arrange
inner_step_type = MockStepType
state = MagicMock(spec=KernelProcessStepState)
edge = MagicMock(spec=KernelProcessEdge)
edges = {"output": [edge]}
# Act & Assert
with pytest.raises(ValidationError):
KernelProcessStepInfo(inner_step_type=None, state=state, output_edges=edges)
with pytest.raises(ValidationError):
KernelProcessStepInfo(inner_step_type=inner_step_type, state=None, output_edges=edges)
with pytest.raises(ValidationError):
KernelProcessStepInfo(inner_step_type=inner_step_type, state=state, output_edges=None)
def test_edges_property():
# Arrange
inner_step_type = MockStepType
state = MagicMock(spec=KernelProcessStepState)
edge_1 = MagicMock(spec=KernelProcessEdge)
edge_2 = MagicMock(spec=KernelProcessEdge)
edges = {"output": [edge_1, edge_2]}
# Act
step_info = KernelProcessStepInfo(inner_step_type=inner_step_type, state=state, output_edges=edges)
copied_edges = step_info.edges
# Assert
assert copied_edges == edges
assert copied_edges is not edges
assert copied_edges["output"] is not edges["output"]
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import ValidationError
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
def test_initialization_with_name_and_id():
# Arrange
name = "step_state"
step_id = "5678"
# Act
step_state = KernelProcessStepState(name=name, id=step_id, version="1.0")
# Assert
assert step_state.name == name
assert step_state.id == step_id
assert step_state.state is None
def test_initialization_with_name_only():
# Arrange
name = "step_state_without_id"
# Act
step_state = KernelProcessStepState(name=name, version="1.0")
# Assert
assert step_state.name == name
assert step_state.id is None
assert step_state.state is None
def test_setting_step_state_value():
# Arrange
name = "step_state"
state_value = {"status": "in_progress"}
# Act
step_state = KernelProcessStepState(name=name, version="1.0")
step_state.state = state_value
# Assert
assert step_state.state == state_value
def test_initialization_with_invalid_name():
# Arrange
name = 12345 # Invalid type for name
# Act & Assert
with pytest.raises(ValidationError):
KernelProcessStepState(name=name)
@@ -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
@@ -0,0 +1,211 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.process_builder import ProcessBuilder
from semantic_kernel.processes.process_function_target_builder import ProcessFunctionTargetBuilder
from semantic_kernel.processes.process_step_builder import ProcessStepBuilder
from semantic_kernel.processes.process_step_edge_builder import ProcessStepEdgeBuilder
from semantic_kernel.processes.step_utils import get_fully_qualified_name
def _create_builder(name: str) -> ProcessBuilder:
"""Helper to construct a ProcessBuilder instance bypassing BaseModel required fields."""
return ProcessBuilder.model_construct(
name=name,
event_namespace=f"{name}_ns",
id="id",
functions_dict={},
edges={},
entry_steps=[],
external_event_target_map={},
has_parent_process=False,
steps=[],
factories={},
)
class DummyTarget:
function_name = "some_function"
parameter_name = "some_param"
class DummyStep(KernelProcessStep):
def resolve_function_target(self, function_name, parameter_name):
return DummyTarget()
class SampleEnum(Enum):
"""Sample enum for event IDs."""
EVENT_A = "event_a"
def test_add_step_with_non_class_raises() -> None:
"""Test that add_step raises when a non-class is passed as step_type."""
builder = _create_builder("testProc")
with pytest.raises(ProcessInvalidConfigurationException) as exc_info:
builder.add_step(123) # type: ignore
assert "Expected a class type" in str(exc_info.value)
def test_add_step_appends_step_and_returns_builder() -> None:
"""Test that a valid class step is appended and returned correctly."""
builder = _create_builder("proc1")
initial_state = {"key": "value"}
step_builder = builder.add_step(DummyStep, name="CustomStep", initial_state=initial_state)
assert step_builder in builder.steps
assert step_builder.name == "CustomStep"
assert step_builder.initial_state == initial_state
def test_add_step_with_factory_function_stores_factory() -> None:
"""Test that providing a factory function stores it in the factories dict."""
builder = _create_builder("proc2")
def factory_func() -> DummyStep:
return DummyStep()
builder.add_step(DummyStep, factory_function=factory_func)
fqn = get_fully_qualified_name(DummyStep)
assert fqn in builder.factories
assert builder.factories[fqn] is factory_func
def test_where_input_event_missing_raises() -> None:
"""Test that where_input_event raises when the event ID is not registered."""
builder = _create_builder("proc")
with pytest.raises(ValueError) as exc_info:
builder.where_input_event_is("nonexistent")
assert "does not expose an event with Id" in str(exc_info.value)
def test_where_input_event_success_returns_copy() -> None:
"""Test that where_input_event returns a copy with updated step and event id."""
builder = _create_builder("proc")
builder.entry_steps.append(DummyStep())
orig = ProcessFunctionTargetBuilder(builder)
builder.external_event_target_map["evt1"] = orig
class Evt(Enum):
EVT1 = "evt1"
result = builder.where_input_event_is(Evt.EVT1)
assert result is not orig
assert result.step is builder
assert result.target_event_id == "evt1"
def test_on_input_event_returns_edge_builder_with_string_and_enum() -> None:
"""Test that on_input_event returns a ProcessEdgeBuilder with correct source and event_id."""
from semantic_kernel.processes.process_builder import ProcessEdgeBuilder # noqa: F811, I001
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata # noqa: F401
from semantic_kernel.processes.process_builder import ProcessBuilder # noqa: F401
ProcessEdgeBuilder.model_rebuild()
builder = _create_builder("proc")
eb_str = builder.on_input_event("myEvent")
assert isinstance(eb_str, ProcessEdgeBuilder)
assert eb_str.source is builder
assert eb_str.event_id == "myEvent"
eb_enum = builder.on_input_event(SampleEnum.EVENT_A)
assert eb_enum.event_id == SampleEnum.EVENT_A.value
def test_link_to_raises_when_target_none() -> None:
"""Test that link_to raises when edge_builder.target is not set."""
builder = _create_builder("proc")
edge = ProcessStepEdgeBuilder(source=builder, event_id="evt")
with pytest.raises(ValueError) as exc_info:
builder.link_to("evt", edge)
assert "Target must be set before linking" in str(exc_info.value)
def test_link_to_success_updates_internal_maps_and_edges() -> None:
"""Test that link_to properly updates entry_steps, external_event_target_map, and edges."""
builder = _create_builder("proc")
edge = ProcessStepEdgeBuilder(source=builder, event_id="evt")
builder.entry_steps.append(DummyStep())
target_builder = ProcessFunctionTargetBuilder(builder)
edge.target = target_builder
builder.link_to("evt", edge)
assert edge.source in builder.entry_steps
assert builder.external_event_target_map["evt"] is target_builder
assert "evt" in builder.edges
assert edge in builder.edges["evt"]
def test_build_with_simple_step_yields_kernel_process() -> None:
"""Test that build returns a KernelProcess with correct properties for simple steps."""
builder = _create_builder("simpleProc")
builder.add_step(DummyStep)
process = builder.build()
assert isinstance(process, KernelProcess)
assert process.state.name == "simpleProc"
assert process.state.id is None
assert len(process.steps) == 1
step_info = process.steps[0]
assert step_info.inner_step_type is DummyStep
assert step_info.output_edges == {}
assert process.factories == {}
def test_resolve_function_target_no_steps_raises() -> None:
"""Test that resolve_function_target raises when there are no entry_steps."""
builder = _create_builder("proc")
with pytest.raises(ValueError) as exc_info:
builder.resolve_function_target("fn", "param")
assert "No targets found for function" in str(exc_info.value)
def test_resolve_function_target_multiple_targets_raises() -> None:
"""Test that resolve_function_target raises when multiple entry_steps return targets."""
builder = _create_builder("proc")
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata # noqa: F401
from semantic_kernel.processes.process_builder import ProcessBuilder # noqa: F401
ProcessBuilder.model_rebuild()
class FakeStep(ProcessStepBuilder):
def __init__(self, name: str = "default_step_name"):
super().__init__(name=name)
def resolve_function_target(self, fn: str, pn: str) -> KernelProcessFunctionTarget:
return KernelProcessFunctionTarget(step_id="1", function_name=fn, parameter_name=pn)
builder.entry_steps = [FakeStep(), FakeStep()] # type: ignore
with pytest.raises(ValueError) as exc_info:
builder.resolve_function_target("fn", "param")
assert "Multiple targets found for function" in str(exc_info.value)
def test_resolve_function_target_success() -> None:
"""Test that resolve_function_target returns the single target when exactly one entry_step matches."""
builder = _create_builder("proc")
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata # noqa: F401
from semantic_kernel.processes.process_builder import ProcessBuilder # noqa: F401
ProcessBuilder.model_rebuild()
class FakeStep(ProcessStepBuilder):
def __init__(self, name: str = "default_step_name"):
super().__init__(name=name)
def resolve_function_target(self, fn: str, pn: str) -> KernelProcessFunctionTarget:
return KernelProcessFunctionTarget(step_id="1", function_name=fn, parameter_name=pn)
builder.entry_steps = [FakeStep()] # type: ignore
result = builder.resolve_function_target("myFn", "myParam")
assert isinstance(result, KernelProcessFunctionTarget)
assert result.step_id == "1"
assert result.function_name == "myFn"
assert result.parameter_name == "myParam"
@@ -0,0 +1,163 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
import pytest
from semantic_kernel.processes.process_builder import ProcessBuilder
from semantic_kernel.processes.process_edge_builder import ProcessEdgeBuilder
from semantic_kernel.processes.process_function_target_builder import (
ProcessFunctionTargetBuilder,
)
from semantic_kernel.processes.process_step_builder import ProcessStepBuilder
from semantic_kernel.processes.process_step_edge_builder import ProcessStepEdgeBuilder
@pytest.fixture(scope="function", autouse=True)
def rebuild_model():
"""Fixture to rebuild the model before each test."""
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata # noqa: F401
from semantic_kernel.processes.process_builder import ProcessBuilder # noqa: F401
ProcessEdgeBuilder.model_rebuild()
def test_initialization():
from semantic_kernel.processes.process_builder import ProcessBuilder # noqa: F401
ProcessEdgeBuilder.model_rebuild()
# Arrange
source = MagicMock(spec=ProcessBuilder)
event_id = "event_001"
# Act
edge_builder = ProcessEdgeBuilder(source=source, event_id=event_id)
# Assert
assert edge_builder.source == source
assert edge_builder.event_id == event_id
assert edge_builder.target is None
def test_send_event_to_with_target():
# Arrange
source = MagicMock(spec=ProcessBuilder)
source.link_to = MagicMock()
target = MagicMock(spec=ProcessFunctionTargetBuilder)
event_id = "event_002"
edge_builder = ProcessEdgeBuilder(source=source, event_id=event_id)
# Act
result = edge_builder.send_event_to(target)
# Assert
assert edge_builder.target == target
assert isinstance(result, ProcessEdgeBuilder)
linked_edge_builder = source.link_to.call_args[0][1]
assert isinstance(linked_edge_builder, ProcessStepEdgeBuilder)
assert linked_edge_builder.event_id == event_id
assert linked_edge_builder.source == source
assert linked_edge_builder.target == target
def test_send_event_to_with_step_builder():
# Arrange
source = MagicMock(spec=ProcessBuilder)
source.link_to = MagicMock()
target_step = MagicMock(spec=ProcessStepBuilder)
target_step.resolve_function_target.return_value = MagicMock(
function_name="process_data", parameter_name="input_param"
)
event_id = "event_003"
edge_builder = ProcessEdgeBuilder(source=source, event_id=event_id)
# Act
result = edge_builder.send_event_to(target_step, parameter_name="input_param")
# Assert
assert isinstance(edge_builder.target, ProcessFunctionTargetBuilder)
assert edge_builder.target.step == target_step
assert edge_builder.target.parameter_name == "input_param"
assert isinstance(result, ProcessEdgeBuilder)
# Capture the ProcessStepEdgeBuilder that should have been linked
linked_edge_builder = source.link_to.call_args[0][1]
assert isinstance(linked_edge_builder, ProcessStepEdgeBuilder)
assert linked_edge_builder.event_id == event_id
assert linked_edge_builder.source == source
assert linked_edge_builder.target == edge_builder.target
def test_send_event_to_step_with_multiple_functions():
from semantic_kernel.functions.kernel_function_metadata import (
KernelFunctionMetadata,
) # noqa: F401
# Arrange
source = MagicMock(spec=ProcessBuilder)
source.link_to = MagicMock()
target_step = ProcessStepBuilder(name="test_step")
target_step.functions_dict = {
"func_1": MagicMock(spec=KernelFunctionMetadata),
"func_2": MagicMock(spec=KernelFunctionMetadata),
}
event_id = "event_004"
edge_builder = ProcessEdgeBuilder(source=source, event_id=event_id)
# Act - Create edges to both functions in the step
result1 = edge_builder.send_event_to(target_step, function_name="func_1", parameter_name="input_param1")
result2 = edge_builder.send_event_to(target_step, function_name="func_2", parameter_name="input_param2")
# Assert
# Verify both edges were created
assert len(source.link_to.call_args_list) == 2
# Check first edge
first_edge = source.link_to.call_args_list[0][0][1]
assert isinstance(first_edge, ProcessStepEdgeBuilder)
assert first_edge.target.function_name == "func_1"
assert first_edge.target.parameter_name == "input_param1"
assert first_edge.target.step == target_step
assert isinstance(result1, ProcessEdgeBuilder)
# Check second edge
second_edge = source.link_to.call_args_list[1][0][1]
assert isinstance(second_edge, ProcessStepEdgeBuilder)
assert second_edge.target.function_name == "func_2"
assert second_edge.target.parameter_name == "input_param2"
assert second_edge.target.step == target_step
assert isinstance(result2, ProcessEdgeBuilder)
def test_send_event_to_creates_step_edge():
# Arrange
source = MagicMock(spec=ProcessBuilder)
source.link_to = MagicMock()
target = MagicMock(spec=ProcessFunctionTargetBuilder)
event_id = "event_005"
edge_builder = ProcessEdgeBuilder(source=source, event_id=event_id)
# Act
result = edge_builder.send_event_to(target)
# Assert
assert edge_builder.target == target
assert isinstance(result, ProcessEdgeBuilder)
source.link_to.assert_called_once()
assert isinstance(source.link_to.call_args[0][1], ProcessStepEdgeBuilder)
assert source.link_to.call_args[0][1].event_id == event_id
def test_send_event_to_raises_error_on_invalid_target():
# Arrange
source = MagicMock(spec=ProcessBuilder)
event_id = "event_006"
edge_builder = ProcessEdgeBuilder(source=source, event_id=event_id)
# Act & Assert
with pytest.raises(TypeError):
edge_builder.send_event_to(None)
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
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.process_message_factory import ProcessMessageFactory
def test_create_from_edge():
"""Test initialization of KernelProcessEdge with valid input."""
source_step_id = "step_1"
output_target = KernelProcessFunctionTarget(
step_id="step_2", function_name="process_data", parameter_name="input_data", target_event_id="event_1"
)
edge = KernelProcessEdge(source_step_id=source_step_id, output_target=output_target)
process_message = ProcessMessageFactory.create_from_edge(edge, "data")
assert process_message.source_id == source_step_id
@@ -0,0 +1,205 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from unittest.mock import MagicMock
import pytest
from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
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.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
from semantic_kernel.processes.process_step_builder import ProcessStepBuilder
from semantic_kernel.processes.process_step_edge_builder import ProcessStepEdgeBuilder
class TestFunctionEnum(Enum):
MY_FUNCTION = "my_function"
class MockKernelProcessStep(KernelProcessStep):
"""A mock class to use as a step type."""
pass
def test_initialization():
# Arrange
name = "test_step"
step_type = MockKernelProcessStep
initial_state = MagicMock()
# Act
step_builder = ProcessStepBuilder(name=name, type=step_type, initial_state=initial_state)
# Assert
assert step_builder.name == name
assert step_builder.function_type == step_type
assert step_builder.initial_state == initial_state
assert step_builder.id is not None
assert step_builder.event_namespace.startswith(name)
def test_initialization_without_type():
# Arrange
name = "test_step_no_type"
# Act
step_builder = ProcessStepBuilder(name=name)
# Assert
assert step_builder.name == name
assert step_builder.function_type is None
assert step_builder.initial_state is None
assert step_builder.id is not None
assert step_builder.event_namespace.startswith(name)
def test_on_input_event():
# Arrange
name = "test_step"
event_id = "event_input"
step_builder = ProcessStepBuilder(name=name)
# Act
edge_builder = step_builder.on_input_event(event_id=event_id)
# Assert
assert isinstance(edge_builder, ProcessStepEdgeBuilder)
assert edge_builder.source == step_builder
assert edge_builder.event_id == event_id
def test_on_event():
# Arrange
name = "test_step"
event_id = "event_action"
step_builder = ProcessStepBuilder(name=name)
# Act
edge_builder = step_builder.on_event(event_id=event_id)
# Assert
assert isinstance(edge_builder, ProcessStepEdgeBuilder)
assert edge_builder.source == step_builder
assert edge_builder.event_id == f"{step_builder.event_namespace}.{event_id}"
def test_resolve_function_target_with_single_function():
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name)
step_builder.functions_dict = {"process_data": MagicMock(spec=KernelFunctionMetadata)}
# Act
target = step_builder.resolve_function_target(function_name=None, parameter_name="input_param")
# Assert
assert target.function_name == "process_data"
assert target.parameter_name == "input_param"
def test_resolve_function_target_with_multiple_functions():
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name)
step_builder.functions_dict = {
"func_1": MagicMock(spec=KernelFunctionMetadata),
"func_2": MagicMock(spec=KernelFunctionMetadata),
}
# Act & Assert
with pytest.raises(
KernelException, match="The target step has more than one function, so a function name must be provided."
):
step_builder.resolve_function_target(function_name=None, parameter_name="input_param")
def test_get_scoped_event_id():
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name)
event_id = "my_event"
# Act
scoped_event_id = step_builder.get_scoped_event_id(event_id)
# Assert
assert scoped_event_id == f"{step_builder.event_namespace}.{event_id}"
def test_build_step():
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name, type=MockKernelProcessStep)
edge = MagicMock(spec=KernelProcessEdge)
built_edge = KernelProcessEdge(
source_step_id="source_step_id",
output_target=MagicMock(spec=KernelProcessFunctionTarget),
)
edge.build = MagicMock(return_value=built_edge)
step_builder.edges = {"event_1": [edge]}
# Act
step_info = step_builder.build_step()
# Assert
assert isinstance(step_info, KernelProcessStepInfo)
assert step_info.state.name == step_builder.name
assert step_info.state.id == step_builder.id
assert step_info.output_edges == {"event_1": [built_edge]}
def test_link_to():
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name)
edge_builder = MagicMock(spec=ProcessStepEdgeBuilder)
event_id = "event_1"
# Act
step_builder.link_to(event_id, edge_builder)
# Assert
assert step_builder.edges[event_id] == [edge_builder]
def test_link_to_multiple_edges():
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name)
edge_builder_1 = MagicMock(spec=ProcessStepEdgeBuilder)
edge_builder_2 = MagicMock(spec=ProcessStepEdgeBuilder)
event_id = "event_2"
# Act
step_builder.link_to(event_id, edge_builder_1)
step_builder.link_to(event_id, edge_builder_2)
# Assert
assert step_builder.edges[event_id] == [edge_builder_1, edge_builder_2]
@pytest.mark.parametrize(
"function_name, expected_function_name",
[
("my_function", "my_function"),
(TestFunctionEnum.MY_FUNCTION, TestFunctionEnum.MY_FUNCTION.value),
],
)
def test_on_function_result(function_name, expected_function_name):
# Arrange
name = "test_step"
step_builder = ProcessStepBuilder(name=name)
# Act
edge_builder = step_builder.on_function_result(function_name=function_name)
# Assert
assert isinstance(edge_builder, ProcessStepEdgeBuilder)
assert edge_builder.source == step_builder
assert edge_builder.event_id == f"{step_builder.event_namespace}.{expected_function_name}.OnResult"
@@ -0,0 +1,158 @@
# 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.process_function_target_builder import ProcessFunctionTargetBuilder
from semantic_kernel.processes.process_step_builder import ProcessStepBuilder
from semantic_kernel.processes.process_step_edge_builder import ProcessStepEdgeBuilder
def test_initialization():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
event_id = "event_001"
# Act
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Assert
assert edge_builder.source == source
assert edge_builder.event_id == event_id
assert edge_builder.target is None
def test_send_event_to():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.link_to = MagicMock()
target = MagicMock(spec=ProcessFunctionTargetBuilder)
event_id = "event_002"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Act
edge_builder.send_event_to(target)
# Assert
assert edge_builder.target == target
source.link_to.assert_called_once_with(event_id, edge_builder)
def test_send_event_to_step_builder_input():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.link_to = MagicMock()
target = MagicMock(spec=ProcessStepBuilder)
target.resolve_function_target = MagicMock(
return_value=MagicMock(function_name="mock_function_name", parameter_name="provided_parameter_name")
)
event_id = "event_003"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Act
edge_builder.send_event_to(target, parameter_name="provided_parameter_name")
# Assert
assert edge_builder.target.step == target
assert edge_builder.target.parameter_name == "provided_parameter_name"
source.link_to.assert_called_once_with(event_id, edge_builder)
def test_send_event_to_creates_target():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.link_to = MagicMock()
target_step = MagicMock(spec=ProcessStepBuilder)
target_step.resolve_function_target.return_value = MagicMock(
function_name="process_data", parameter_name="input_param"
)
event_id = "event_003"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Act
edge_builder.send_event_to(target_step, parameter_name="input_param")
# Assert
assert isinstance(edge_builder.target, ProcessFunctionTargetBuilder)
assert edge_builder.target.step == target_step
assert edge_builder.target.function_name == "process_data"
assert edge_builder.target.parameter_name == "input_param"
source.link_to.assert_called_once_with(event_id, edge_builder)
def test_stop_process():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.link_to = MagicMock()
event_id = "event_004"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Act
edge_builder.stop_process()
# Assert
assert edge_builder.target is not None
assert edge_builder.target.function_name == "END"
source.link_to.assert_called_once_with("END", edge_builder)
def test_build():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.id = "source_step_id"
source.name = "source_step_name"
target = MagicMock(spec=ProcessFunctionTargetBuilder)
target.build = MagicMock(return_value=MagicMock(spec=KernelProcessFunctionTarget))
event_id = "event_005"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
edge_builder.target = target
# Act
edge = edge_builder.build()
# Assert
assert isinstance(edge, KernelProcessEdge)
assert edge.source_step_id == source.id
assert edge.output_target == target.build()
def test_build_missing_target():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.id = "source_step_id"
source.name = "source_step_name"
event_id = "event_006"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Act & Assert
with pytest.raises(ValueError, match="Target must be provided"):
edge_builder.build()
def test_send_event_to_step_builder_with_function_name():
# Arrange
source = MagicMock(spec=ProcessStepBuilder)
source.link_to = MagicMock()
target = MagicMock(spec=ProcessStepBuilder)
target.resolve_function_target = MagicMock(
return_value=MagicMock(function_name="mock_function_name", parameter_name="mock_parameter_name")
)
event_id = "event_007"
edge_builder = ProcessStepEdgeBuilder(source=source, event_id=event_id)
# Act
edge_builder.send_event_to(target, parameter_name="mock_parameter_name", function_name="mock_function_name")
# Assert
assert edge_builder.target.step == target
assert edge_builder.target.parameter_name == "mock_parameter_name"
assert edge_builder.target.function_name == "mock_function_name"
source.link_to.assert_called_once_with(event_id, edge_builder)
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TypeVar
from unittest.mock import patch
from semantic_kernel.processes.process_types import get_generic_state_type
# Test classes for various scenarios
class ConcreteState:
pass
class ConcreteKernelProcessStep:
state: ConcreteState
class OptionalStateKernelProcessStep:
state: ConcreteState | None
class OptionalStateKernelProcessStepOldSyntax:
state: ConcreteState | None
TState = TypeVar("TState")
class TypeVarStateKernelProcessStep:
state: TState
class NoStateKernelProcessStep:
pass
class InheritedKernelProcessStep(ConcreteKernelProcessStep):
pass
def test_get_generic_state_type_concrete():
result = get_generic_state_type(ConcreteKernelProcessStep)
assert result is ConcreteState
def test_get_generic_state_type_optional():
result = get_generic_state_type(OptionalStateKernelProcessStep)
assert result is ConcreteState
def test_get_generic_state_type_optional_old_syntax():
result = get_generic_state_type(OptionalStateKernelProcessStepOldSyntax)
assert result is ConcreteState
def test_get_generic_state_type_typevar():
result = get_generic_state_type(TypeVarStateKernelProcessStep)
assert result is None
def test_get_generic_state_type_no_state():
result = get_generic_state_type(NoStateKernelProcessStep)
assert result is None
def test_get_generic_state_type_inherited():
result = get_generic_state_type(InheritedKernelProcessStep)
assert result is ConcreteState
def test_get_generic_state_type_exception():
with patch("semantic_kernel.processes.process_types.get_type_hints", side_effect=Exception("Mocked exception")):
result = get_generic_state_type(ConcreteKernelProcessStep)
assert result is None
@@ -0,0 +1,204 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Callable
from typing import Any
from unittest.mock import MagicMock
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import (
KernelProcessMessageChannel,
)
from semantic_kernel.processes.kernel_process.kernel_process_step_context import (
KernelProcessStepContext,
)
from semantic_kernel.processes.step_utils import (
find_input_channels,
get_fully_qualified_name,
get_step_class_from_qualified_name,
)
@pytest.fixture
def mock_function() -> Callable[..., Any]:
@kernel_function
def mock_function(input: str, context: KernelProcessStepContext) -> None:
pass
return mock_function
def test_find_input_channels_empty_functions():
channel = MagicMock(spec=KernelProcessMessageChannel)
functions = {}
with pytest.raises(ValueError) as exc_info:
find_input_channels(channel, functions)
assert str(exc_info.value) == "The step has not been initialized."
def test_find_input_channels_with_required_params(mock_function):
channel = MagicMock(spec=KernelProcessMessageChannel)
function = KernelFunction.from_method(method=mock_function, plugin_name="test")
functions = {"TestFunction": function}
inputs = find_input_channels(channel, functions)
assert "TestFunction" in inputs
assert "input" in inputs["TestFunction"]
assert "context" in inputs["TestFunction"]
@kernel_function
def skipped_function(param1: "Kernel", param2: int = 0) -> None:
pass
def test_find_input_channels_with_only_skipped_params():
channel = MagicMock(spec=KernelProcessMessageChannel)
function = KernelFunction.from_method(method=skipped_function, plugin_name="test")
functions = {"SkippedFunction": function}
inputs = find_input_channels(channel, functions)
expected_inputs = {"SkippedFunction": {}}
assert inputs == expected_inputs
@kernel_function
def function1(param1: str) -> None:
pass
@kernel_function
def function2(param2: KernelProcessStepContext, param3: float = 0.0) -> None:
pass
def test_find_input_channels_multiple_functions():
channel = MagicMock(spec=KernelProcessMessageChannel)
function1_instance = KernelFunction.from_method(method=function1, plugin_name="test")
function2_instance = KernelFunction.from_method(method=function2, plugin_name="test")
functions = {
"Function1": function1_instance,
"Function2": function2_instance,
}
inputs = find_input_channels(channel, functions)
assert "Function1" in inputs
assert "param1" in inputs["Function1"]
assert inputs["Function1"]["param1"] is None
assert "Function2" in inputs
assert "param2" in inputs["Function2"]
assert isinstance(inputs["Function2"]["param2"], KernelProcessStepContext)
assert "param3" not in inputs["Function2"]
def test_get_fully_qualified_name_standard_class():
class TestClass:
pass
result = get_fully_qualified_name(TestClass)
expected = f"{TestClass.__module__}.TestClass"
assert result == expected
def test_get_fully_qualified_name_builtin_type():
result = get_fully_qualified_name(int)
expected = "builtins.int"
assert result == expected
def test_get_fully_qualified_name_third_party_class():
from unittest.mock import MagicMock
result = get_fully_qualified_name(MagicMock)
expected = "unittest.mock.MagicMock"
assert result == expected
def test_get_fully_qualified_name_nested_class():
class OuterClass:
class InnerClass:
pass
result = get_fully_qualified_name(OuterClass.InnerClass)
expected = f"{OuterClass.__module__}.InnerClass"
assert result == expected
# --- Tests for get_step_class_from_qualified_name ---
def test_get_step_class_empty_allowlist_blocks_all():
"""An empty allowlist sequence should block all modules."""
with pytest.raises(ProcessInvalidConfigurationException, match="not in the allowed module prefixes"):
get_step_class_from_qualified_name(
"semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
allowed_module_prefixes=[],
)
def test_get_step_class_none_allowlist_allows_any():
"""Passing None should disable the allowlist check entirely."""
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
cls = get_step_class_from_qualified_name(
"semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
allowed_module_prefixes=None,
)
assert cls is KernelProcessStep
def test_get_step_class_prefix_without_dot_exact_match():
"""A prefix without a trailing dot should match the exact module name."""
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
cls = get_step_class_from_qualified_name(
"semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
allowed_module_prefixes=["semantic_kernel.processes.kernel_process.kernel_process_step"],
)
assert cls is KernelProcessStep
def test_get_step_class_prefix_without_dot_segment_boundary():
"""A prefix without a trailing dot must not match partial segment names."""
with pytest.raises(ProcessInvalidConfigurationException, match="not in the allowed module prefixes"):
get_step_class_from_qualified_name(
"semantic_kernel_evil.some_module.SomeClass",
allowed_module_prefixes=["semantic_kernel"],
)
def test_get_step_class_prefix_with_dot_matches_submodule():
"""A prefix with a trailing dot should match submodules."""
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
cls = get_step_class_from_qualified_name(
"semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
allowed_module_prefixes=["semantic_kernel."],
)
assert cls is KernelProcessStep
def test_get_step_class_default_allowlist_blocks_non_sk_module():
"""Default allowlist should block modules outside semantic_kernel."""
with pytest.raises(ProcessInvalidConfigurationException, match="not in the allowed module prefixes"):
get_step_class_from_qualified_name("os.path.SomeClass")
def test_get_step_class_invalid_format():
"""Invalid class name format should raise an exception."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name("NoModule")