205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
# 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")
|