chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user