chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

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