diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs index d0a63da1a274..9ead1b39167c 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs @@ -278,6 +278,10 @@ internal ProcessStepBuilderTyped(Type stepType, string id, ProcessBuilder? proce : base(id, processBuilder) { Verify.NotNull(stepType); + if (!typeof(KernelProcessStep).IsAssignableFrom(stepType)) + { + throw new ArgumentException($"Type '{stepType.FullName}' must be a subclass of KernelProcessStep.", nameof(stepType)); + } this._stepType = stepType; this.FunctionsDict = this.GetFunctionMetadataMap(); diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/DaprStepInfoTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/DaprStepInfoTests.cs new file mode 100644 index 000000000000..60b81ab16428 --- /dev/null +++ b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/DaprStepInfoTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.Process.Dapr.Runtime.UnitTests; + +/// +/// Unit tests for the class. +/// +public class DaprStepInfoTests +{ + /// + /// Tests that ToKernelProcessStepInfo throws when InnerStepDotnetType is not a KernelProcessStep subclass. + /// + [Fact] + public void ToKernelProcessStepInfoThrowsForInvalidStepType() + { + // Arrange + var stepInfo = new DaprStepInfo + { + InnerStepDotnetType = typeof(string).AssemblyQualifiedName!, + State = new KernelProcessStepState("TestStep", version: "v1"), + Edges = new Dictionary>() + }; + + // Act & Assert + var ex = Assert.Throws(() => stepInfo.ToKernelProcessStepInfo()); + Assert.Contains("is not a valid KernelProcessStep type", ex.Message); + } + + /// + /// Tests that ToKernelProcessStepInfo throws when InnerStepDotnetType cannot be resolved. + /// + [Fact] + public void ToKernelProcessStepInfoThrowsForUnresolvableType() + { + // Arrange + var stepInfo = new DaprStepInfo + { + InnerStepDotnetType = "NonExistent.Type, NonExistent.Assembly", + State = new KernelProcessStepState("TestStep", version: "v1"), + Edges = new Dictionary>() + }; + + // Act & Assert + Assert.Throws(() => stepInfo.ToKernelProcessStepInfo()); + } + + /// + /// Tests that ToKernelProcessStepInfo succeeds for a valid KernelProcessStep subclass. + /// + [Fact] + public void ToKernelProcessStepInfoSucceedsForValidStepType() + { + // Arrange + var stepInfo = new DaprStepInfo + { + InnerStepDotnetType = typeof(ValidTestStep).AssemblyQualifiedName!, + State = new KernelProcessStepState("TestStep", version: "v1"), + Edges = new Dictionary>() + }; + + // Act + var result = stepInfo.ToKernelProcessStepInfo(); + + // Assert + Assert.NotNull(result); + Assert.Equal(typeof(ValidTestStep), result.InnerStepType); + } + + /// + /// A valid test step for type validation testing. + /// + public sealed class ValidTestStep : KernelProcessStep + { + /// + /// A test function. + /// + [KernelFunction] + public void TestFunction() + { + } + } +} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TypeInfoTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TypeInfoTests.cs new file mode 100644 index 000000000000..24e532b412f7 --- /dev/null +++ b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TypeInfoTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Process.Serialization; +using Xunit; + +namespace SemanticKernel.Process.Dapr.Runtime.UnitTests; + +/// +/// Unit tests for the class. +/// +public class TypeInfoTests +{ + /// + /// Tests that ConvertValue deserializes a JsonElement to the correct type. + /// + [Fact] + public void ConvertValueDeserializesJsonElement() + { + // Arrange + var json = JsonSerializer.SerializeToElement(42); + var typeName = typeof(int).AssemblyQualifiedName; + + // Act + var result = TypeInfo.ConvertValue(typeName, json); + + // Assert + Assert.Equal(42, result); + } + + /// + /// Tests that ConvertValue throws when the type name cannot be resolved. + /// + [Fact] + public void ConvertValueThrowsForUnresolvableType() + { + // Arrange + var json = JsonSerializer.SerializeToElement(42); + + // Act & Assert + Assert.Throws(() => + TypeInfo.ConvertValue("NonExistent.Type, NonExistent.Assembly", json)); + } + + /// + /// Tests that ConvertValue returns non-JsonElement values unchanged. + /// + [Fact] + public void ConvertValueReturnsNonJsonElementUnchanged() + { + // Arrange + var value = "plain string"; + + // Act + var result = TypeInfo.ConvertValue(typeof(string).AssemblyQualifiedName, value); + + // Assert + Assert.Equal("plain string", result); + } + + /// + /// Tests that ConvertValue returns null when value is null. + /// + [Fact] + public void ConvertValueReturnsNullWhenValueIsNull() + { + // Act + var result = TypeInfo.ConvertValue(typeof(string).AssemblyQualifiedName, null); + + // Assert + Assert.Null(result); + } +} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs index 27bda37176fb..ff8545efb73b 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs @@ -102,6 +102,11 @@ private void InitializeStep(DaprStepInfo stepInfo, string? parentProcessId, stri throw new KernelException($"Could not load the inner step type '{stepInfo.InnerStepDotnetType}'.").Log(this._logger); } + if (!typeof(KernelProcessStep).IsAssignableFrom(this._innerStepType)) + { + throw new KernelException($"Type '{stepInfo.InnerStepDotnetType}' is not a valid KernelProcessStep type.").Log(this._logger); + } + this.ParentProcessId = parentProcessId; this._stepInfo = stepInfo; this._stepState = this._stepInfo.State; @@ -373,8 +378,18 @@ protected virtual async ValueTask ActivateStepAsync() if (stepStateType.HasValue) { stateType = Type.GetType(stepStateType.Value); + if (stateType is null) + { + throw new KernelException($"Type '{stepStateType.Value}' could not be resolved to a valid KernelProcessStepState type.").Log(this._logger); + } + + if (!typeof(KernelProcessStepState).IsAssignableFrom(stateType)) + { + throw new KernelException($"Type '{stepStateType.Value}' is not a valid KernelProcessStepState type.").Log(this._logger); + } + var stateObjectJson = await this.StateManager.GetStateAsync(ActorStateKeys.StepStateJson).ConfigureAwait(false); - stateObject = JsonSerializer.Deserialize(stateObjectJson, stateType!) as KernelProcessStepState; + stateObject = JsonSerializer.Deserialize(stateObjectJson, stateType) as KernelProcessStepState; } else { diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs index 15b72d76744d..93a30df4008a 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs @@ -50,6 +50,11 @@ public KernelProcessStepInfo ToKernelProcessStepInfo() throw new KernelException($"Unable to create inner step type from assembly qualified name `{this.InnerStepDotnetType}`"); } + if (!typeof(KernelProcessStep).IsAssignableFrom(innerStepType)) + { + throw new KernelException($"Type '{this.InnerStepDotnetType}' is not a valid KernelProcessStep type."); + } + return new KernelProcessStepInfo(innerStepType, this.State, this.Edges); } diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs index ad64a1e1a53c..9cbca889783b 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs @@ -39,6 +39,11 @@ internal static class TypeInfo } Type? valueType = Type.GetType(assemblyQualifiedTypeName); - return ((JsonElement)value).Deserialize(valueType!); + if (valueType is null) + { + throw new KernelException($"Could not load type '{assemblyQualifiedTypeName}'."); + } + + return ((JsonElement)value).Deserialize(valueType); } } diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs index 26c2fc9b0e7e..8d8ea820513b 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs @@ -167,6 +167,37 @@ public void OnFunctionErrorCreatesEdgeBuilder() Assert.EndsWith("Global.OnError", edgeBuilder.EventData.EventId); } + /// + /// Tests that AddStepFromType(Type) throws ArgumentException for non-KernelProcessStep types. + /// + [Fact] + public void AddStepFromTypeWithInvalidTypeThrowsArgumentException() + { + // Arrange + var processBuilder = new ProcessBuilder(ProcessName); + + // Act & Assert + var ex = Assert.Throws(() => processBuilder.AddStepFromType(typeof(string))); + Assert.Contains("must be a subclass of KernelProcessStep", ex.Message); + } + + /// + /// Tests that AddStepFromType(Type) succeeds for valid KernelProcessStep types. + /// + [Fact] + public void AddStepFromTypeWithValidTypeAddsStep() + { + // Arrange + var processBuilder = new ProcessBuilder(ProcessName); + + // Act + var stepBuilder = processBuilder.AddStepFromType(typeof(TestStep), StepName); + + // Assert + Assert.Single(processBuilder.Steps); + Assert.Equal(StepName, stepBuilder.Name); + } + /// /// A class that represents a step for testing. ///