// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Hosting.UnitTests; public class AgentProxyThreadTests { /// /// Provides valid identifier values that conform to RFC 3986 unreserved characters. /// public static IEnumerable ValidIds => new List { new object[] { "normal" }, new object[] { "test-id" }, new object[] { "test_id" }, new object[] { "test.id" }, new object[] { "test~id" }, new object[] { "ABC123" }, new object[] { "a" }, new object[] { "123" }, new object[] { "test-id_with.various~chars" }, new object[] { new string('a', 100) } // Long but valid ID }; /// /// Provides invalid identifier values that violate the RFC 3986 unreserved character rules. /// public static IEnumerable InvalidIds => new List { new object[] { " " }, // Space not allowed new object[] { "!@#$%^&*()" }, // Special characters not allowed new object[] { "test id" }, // Space not allowed new object[] { "test/id" }, // Forward slash not allowed new object[] { "test?id" }, // Question mark not allowed new object[] { "test#id" }, // Hash not allowed new object[] { "test@id" }, // At symbol not allowed new object[] { "test id with spaces" }, // Multiple spaces not allowed new object[] { "test\tid" }, // Tab not allowed new object[] { "test\nid" }, // Newline not allowed }; /// /// Verifies that providing valid id to constructor sets the Id property correctly. /// /// The valid identifier to test. [Theory] [MemberData(nameof(ValidIds))] public void Constructor_ValidId_SetsIdProperty(string id) { // Act var thread = new AgentProxyThread(id); // Assert Assert.Equal(id, thread.ConversationId); } /// /// Verifies that providing invalid id to constructor throws an . /// /// The invalid identifier to test. [Theory] [MemberData(nameof(InvalidIds))] public void Constructor_InvalidId_ThrowsArgumentException(string id) { // Act & Assert var exception = Assert.Throws(() => new AgentProxyThread(id)); Assert.Contains("Thread ID", exception.Message); Assert.Contains("alphanumeric characters, hyphens, underscores, dots, and tildes", exception.Message); } /// /// Verifies that providing a null id to constructor throws an . /// [Fact] public void Constructor_NullId_ThrowsArgumentNullException() { // Act & Assert Assert.Throws(() => new AgentProxyThread(null!)); } /// /// Verifies that providing an empty id to constructor throws an . /// [Fact] public void Constructor_EmptyId_ThrowsArgumentException() { // Act & Assert Assert.Throws(() => new AgentProxyThread("")); } /// /// Verifies that the default constructor initializes the Id property with a valid non-empty GUID string in "N" format. /// [Fact] public void Constructor_Default_AssignsValidGuidStringAsId() { // Arrange & Act var thread = new AgentProxyThread(); // Assert Assert.False(string.IsNullOrEmpty(thread.ConversationId)); Assert.True(Guid.TryParseExact(thread.ConversationId, "N", out _), $"Id '{thread.ConversationId}' is not a valid GUID in 'N' format."); } /// /// Verifies that successive default constructors produce unique Id values. /// [Fact] public void Constructor_Default_CreatesUniqueIds() { // Arrange & Act var thread1 = new AgentProxyThread(); var thread2 = new AgentProxyThread(); // Assert Assert.NotEqual(thread1.ConversationId, thread2.ConversationId); } /// /// Verifies that CreateId returns a non-null, non-empty 32-character hexadecimal string without dashes. /// [Fact] public void CreateId_ReturnsValidHexString() { // Arrange & Act string id = AgentProxyThread.CreateId(); // Assert Assert.False(string.IsNullOrEmpty(id)); Assert.Equal(32, id.Length); Assert.Matches("^[0-9a-f]{32}$", id); } /// /// Verifies that multiple calls to CreateId produce unique identifiers. /// [Fact] public void CreateId_MultipleCalls_ReturnUniqueValues() { // Arrange & Act string id1 = AgentProxyThread.CreateId(); string id2 = AgentProxyThread.CreateId(); // Assert Assert.NotEqual(id1, id2); } /// /// Verifies that ManyCallsInParallel produces unique values across many calls. /// [Fact] public void CreateId_ManyCallsInParallel_AllUnique() { // Arrange const int NumberOfIds = 1000; var ids = new string[NumberOfIds]; // Act - Create IDs in parallel to test thread safety Parallel.For(0, NumberOfIds, i => { ids[i] = AgentProxyThread.CreateId(); }); // Assert var uniqueIds = ids.Distinct().Count(); Assert.Equal(NumberOfIds, uniqueIds); } /// /// Verifies that CreateId generates IDs that pass validation. /// [Fact] public void CreateId_GeneratesValidIds() { // Arrange & Act for (int i = 0; i < 100; i++) { string id = AgentProxyThread.CreateId(); // Assert - Should not throw exception var thread = new AgentProxyThread(id); Assert.Equal(id, thread.ConversationId); } } /// /// Verifies specific edge cases for valid IDs. /// [Theory] [InlineData("a")] [InlineData("1")] [InlineData("_")] [InlineData("-")] [InlineData(".")] [InlineData("~")] [InlineData("a1")] [InlineData("test-123")] [InlineData("my_thread.id~1")] public void Constructor_ValidIdEdgeCases_SetsIdProperty(string id) { // Act var thread = new AgentProxyThread(id); // Assert Assert.Equal(id, thread.ConversationId); } /// /// Verifies specific edge cases for invalid IDs. /// [Theory] [InlineData(" leading-space")] [InlineData("trailing-space ")] [InlineData("with spaces")] [InlineData("with\ttab")] [InlineData("with\nnewline")] [InlineData("with/slash")] [InlineData("with\\backslash")] [InlineData("with%percent")] [InlineData("with+plus")] [InlineData("with=equals")] [InlineData("with?question")] [InlineData("with#hash")] [InlineData("with@at")] [InlineData("with[bracket")] [InlineData("with]bracket")] [InlineData("with{brace")] [InlineData("with}brace")] [InlineData("with(paren")] [InlineData("with)paren")] [InlineData("with!exclamation")] [InlineData("with*asterisk")] [InlineData("with:colon")] [InlineData("with;semicolon")] [InlineData("with,comma")] [InlineData("with\"quote")] [InlineData("with'apostrophe")] public void Constructor_InvalidIdEdgeCases_ThrowsArgumentException(string id) { // Act & Assert var exception = Assert.Throws(() => new AgentProxyThread(id)); Assert.Contains("Thread ID", exception.Message); } /// /// Verifies that AgentProxyThread inherits from AgentThread. /// [Fact] public void AgentProxyThread_InheritsFromAgentThread() { // Arrange & Act var thread = new AgentProxyThread(); // Assert Assert.IsAssignableFrom(thread); } /// /// Verifies that Id property is accessible. /// [Fact] public void Id_IsAccessible() { // Arrange & Act var thread = new AgentProxyThread("test-id"); // Assert Assert.NotNull(thread.ConversationId); Assert.Equal("test-id", thread.ConversationId); } /// /// Verifies that thread ID remains immutable after construction. /// [Fact] public void Id_IsImmutable() { // Arrange const string OriginalId = "immutable-id"; var thread = new AgentProxyThread(OriginalId); // Act & Assert Assert.Equal(OriginalId, thread.ConversationId); } /// /// Verifies that default constructor creates thread with valid GUID format. /// [Fact] public void Constructor_Default_AlwaysCreatesValidGuid() { // Arrange & Act var thread = new AgentProxyThread(); // Assert Assert.NotNull(thread.ConversationId); Assert.Equal(32, thread.ConversationId.Length); Assert.True(Guid.TryParseExact(thread.ConversationId, "N", out var guid)); Assert.NotEqual(Guid.Empty, guid); } }