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

212 lines
8.4 KiB
Python

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