Files
microsoft--semantic-kernel/python/tests/unit/processes/test_step_utils.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

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")