diff --git a/tests/BotSharp.Core.UnitTests/Conversations/ConversationStateServiceExtensionsTests.cs b/tests/BotSharp.Core.UnitTests/Conversations/ConversationStateServiceExtensionsTests.cs
new file mode 100644
index 000000000..138f628eb
--- /dev/null
+++ b/tests/BotSharp.Core.UnitTests/Conversations/ConversationStateServiceExtensionsTests.cs
@@ -0,0 +1,234 @@
+using System.Globalization;
+using BotSharp.Abstraction.Conversations;
+using BotSharp.Abstraction.Conversations.Enums;
+using BotSharp.Abstraction.Infrastructures.Enums;
+using Moq;
+using Xunit;
+
+namespace BotSharp.Core.UnitTests.Conversations;
+
+public class ConversationStateServiceExtensionsTests
+{
+ public enum Color { Red, Green }
+
+ public class Payload
+ {
+ public string Name { get; set; } = "";
+ public int Count { get; set; }
+ }
+
+ ///
+ /// Mirrors ConversationStateService.GetState: a missing key yields the caller's defaultValue.
+ ///
+ private static IConversationStateService States(params (string Key, string Value)[] entries)
+ {
+ var data = entries.ToDictionary(e => e.Key, e => e.Value);
+ var mock = new Mock();
+ mock.Setup(s => s.GetState(It.IsAny(), It.IsAny()))
+ .Returns((string name, string defaultValue) => data.TryGetValue(name, out var v) ? v : defaultValue);
+ return mock.Object;
+ }
+
+ #region GetState
+
+ [Fact]
+ public void GetState_Int_ParsesValue()
+ {
+ Assert.Equal(42, States(("k", "42")).GetState("k"));
+ }
+
+ [Fact]
+ public void GetState_Int_MissingKey_ReturnsDefault()
+ {
+ Assert.Equal(0, States().GetState("k"));
+ Assert.Equal(7, States().GetState("k", 7));
+ }
+
+ [Fact]
+ public void GetState_Int_InvalidValue_ReturnsDefault()
+ {
+ Assert.Equal(7, States(("k", "abc")).GetState("k", 7));
+ }
+
+ [Fact]
+ public void GetState_NullableInt_MissingKey_ReturnsNull()
+ {
+ Assert.Null(States().GetState("k"));
+ }
+
+ [Fact]
+ public void GetState_NullableInt_ParsesValue()
+ {
+ Assert.Equal(5, States(("k", "5")).GetState("k"));
+ }
+
+ [Theory]
+ [InlineData("true", true)]
+ [InlineData("True", true)]
+ [InlineData("false", false)]
+ public void GetState_Bool_ParsesValue(string raw, bool expected)
+ {
+ Assert.Equal(expected, States(("k", raw)).GetState("k"));
+ }
+
+ [Fact]
+ public void GetState_String_ReturnsRawValue()
+ {
+ Assert.Equal("hello", States(("k", "hello")).GetState("k"));
+ }
+
+ [Fact]
+ public void GetState_Decimal_UsesInvariantCultureRegardlessOfCurrentCulture()
+ {
+ var original = CultureInfo.CurrentCulture;
+ try
+ {
+ // fr-FR uses ',' as decimal separator; parsing must still treat '.' as the separator.
+ CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
+ Assert.Equal(1.5m, States(("k", "1.5")).GetState("k"));
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = original;
+ }
+ }
+
+ [Fact]
+ public void GetState_DateTime_PreservesUtcKind()
+ {
+ var result = States(("k", "2026-09-29T10:00:00Z")).GetState("k");
+
+ Assert.Equal(new DateTime(2026, 9, 29, 10, 0, 0, DateTimeKind.Utc), result);
+ Assert.Equal(DateTimeKind.Utc, result.Kind);
+ }
+
+ [Fact]
+ public void GetState_NullableDateTime_MissingKey_ReturnsNull()
+ {
+ Assert.Null(States().GetState("k"));
+ }
+
+ [Theory]
+ [InlineData("Green")]
+ [InlineData("green")]
+ [InlineData("1")]
+ public void GetState_Enum_ParsesNameIgnoringCaseOrNumber(string raw)
+ {
+ Assert.Equal(Color.Green, States(("k", raw)).GetState("k"));
+ }
+
+ [Fact]
+ public void GetState_Enum_InvalidValue_ReturnsDefault()
+ {
+ Assert.Equal(Color.Green, States(("k", "Blue")).GetState("k", Color.Green));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GetState_Object_DeserializesJson(bool useNewtonsoftJson)
+ {
+ var states = States(("k", "{\"name\":\"a\",\"count\":3}"));
+
+ var result = states.GetState("k", useNewtonsoftJson: useNewtonsoftJson);
+
+ Assert.NotNull(result);
+ Assert.Equal("a", result.Name);
+ Assert.Equal(3, result.Count);
+ }
+
+ [Fact]
+ public void GetState_Object_JsonNull_ReturnsDefault()
+ {
+ var fallback = new Payload { Name = "fallback" };
+
+ Assert.Same(fallback, States(("k", "null")).GetState("k", fallback));
+ }
+
+ [Fact]
+ public void GetState_Object_InvalidJson_ReturnsDefault()
+ {
+ Assert.Null(States(("k", "{not json")).GetState("k"));
+ }
+
+ [Fact]
+ public void GetState_Object_MissingKey_ReturnsDefault()
+ {
+ var fallback = new Payload();
+
+ Assert.Same(fallback, States().GetState("k", fallback));
+ }
+
+ [Fact]
+ public void GetState_ListOfInt_DeserializesJsonArray()
+ {
+ Assert.Equal(new List { 1, 2 }, States(("k", "[1,2]")).GetState>("k"));
+ }
+
+ #endregion
+
+ #region Equal / IsTrue / IsNullOrEmpty
+
+ [Fact]
+ public void Equal_And_NotEqual()
+ {
+ var states = States(("k", "v"));
+
+ Assert.True(states.Equal("k", "v"));
+ Assert.False(states.NotEqual("k", "v"));
+ Assert.False(states.Equal("k", "other"));
+ Assert.True(states.NotEqual("k", "other"));
+ }
+
+ [Theory]
+ [InlineData("true", true)]
+ [InlineData("TRUE", true)]
+ [InlineData("false", false)]
+ [InlineData("yes", false)]
+ [InlineData("1", false)]
+ public void IsTrue_And_IsFalse(string raw, bool expected)
+ {
+ var states = States(("k", raw));
+
+ Assert.Equal(expected, states.IsTrue("k"));
+ Assert.Equal(!expected, states.IsFalse("k"));
+ }
+
+ [Fact]
+ public void IsTrue_MissingKey_ReturnsFalse()
+ {
+ Assert.False(States().IsTrue("k"));
+ Assert.True(States().IsFalse("k"));
+ }
+
+ [Fact]
+ public void IsNullOrEmpty_And_IsNotNullOrEmpty()
+ {
+ var states = States(("empty", ""), ("set", "x"));
+
+ Assert.True(states.IsNullOrEmpty("missing"));
+ Assert.True(states.IsNullOrEmpty("empty"));
+ Assert.False(states.IsNullOrEmpty("set"));
+ Assert.True(states.IsNotNullOrEmpty("set"));
+ }
+
+ #endregion
+
+ #region Channels
+
+ [Theory]
+ [InlineData(ConversationChannel.Phone, true, false, false)]
+ [InlineData(ConversationChannel.Email, false, true, false)]
+ [InlineData(ConversationChannel.OpenAPI, false, false, true)]
+ [InlineData("webchat", false, false, false)]
+ public void ChannelChecks(string channel, bool isPhone, bool isEmail, bool isOpenApi)
+ {
+ var states = States((StateConst.CHANNEL, channel));
+
+ Assert.Equal(isPhone, states.IsPhoneChannel());
+ Assert.Equal(isEmail, states.IsEmailChannel());
+ Assert.Equal(isOpenApi, states.IsOpenAPIChannel());
+ }
+
+ #endregion
+}