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