// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; using Aspire.Hosting.ApplicationModel; using Moq; namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests; /// /// Unit tests for the class. /// public class AgentFrameworkBuilderExtensionsTests { #region AddDevUI Validation Tests /// /// Verifies that AddDevUI throws ArgumentNullException when builder is null. /// [Fact] public void AddDevUI_NullBuilder_ThrowsArgumentNullException() { // Act & Assert var exception = Assert.Throws( () => AgentFrameworkBuilderExtensions.AddDevUI(null!, "devui")); Assert.Equal("builder", exception.ParamName); } /// /// Verifies that AddDevUI throws ArgumentNullException when name is null. /// [Fact] public void AddDevUI_NullName_ThrowsArgumentNullException() { // Arrange var builder = DistributedApplication.CreateBuilder(); // Act & Assert var exception = Assert.Throws( () => builder.AddDevUI(null!)); Assert.Equal("name", exception.ParamName); } /// /// Verifies that AddDevUI creates a resource with the specified name. /// [Fact] public void AddDevUI_ValidName_CreatesResourceWithName() { // Arrange var builder = DistributedApplication.CreateBuilder(); // Act var resourceBuilder = builder.AddDevUI("my-devui"); // Assert Assert.Equal("my-devui", resourceBuilder.Resource.Name); } /// /// Verifies that AddDevUI creates a DevUIResource. /// [Fact] public void AddDevUI_ReturnsDevUIResourceBuilder() { // Arrange var builder = DistributedApplication.CreateBuilder(); // Act var resourceBuilder = builder.AddDevUI("devui"); // Assert Assert.IsType(resourceBuilder.Resource); } /// /// Verifies that AddDevUI with port configures the endpoint. /// [Fact] public void AddDevUI_WithPort_ConfiguresEndpointWithPort() { // Arrange var builder = DistributedApplication.CreateBuilder(); // Act var resourceBuilder = builder.AddDevUI("devui", port: 8090); // Assert var endpoint = resourceBuilder.Resource.Annotations .OfType() .FirstOrDefault(e => e.Name == "http"); Assert.NotNull(endpoint); Assert.Equal(8090, endpoint.Port); } /// /// Verifies that AddDevUI without port leaves port as null for dynamic allocation. /// [Fact] public void AddDevUI_WithoutPort_EndpointHasDynamicPort() { // Arrange var builder = DistributedApplication.CreateBuilder(); // Act var resourceBuilder = builder.AddDevUI("devui"); // Assert var endpoint = resourceBuilder.Resource.Annotations .OfType() .FirstOrDefault(e => e.Name == "http"); Assert.NotNull(endpoint); Assert.Null(endpoint.Port); } #endregion #region WithAgentService Validation Tests /// /// Verifies that WithAgentService throws ArgumentNullException when builder is null. /// [Fact] public void WithAgentService_NullBuilder_ThrowsArgumentNullException() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var mockAgentService = CreateMockAgentServiceBuilder(appBuilder, "agent-service"); // Act & Assert var exception = Assert.Throws( () => AgentFrameworkBuilderExtensions.WithAgentService(null!, mockAgentService)); Assert.Equal("builder", exception.ParamName); } /// /// Verifies that WithAgentService throws ArgumentNullException when agentService is null. /// [Fact] public void WithAgentService_NullAgentService_ThrowsArgumentNullException() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); // Act & Assert var exception = Assert.Throws( () => devuiBuilder.WithAgentService(null!)); Assert.Equal("agentService", exception.ParamName); } #endregion #region WithAgentService Annotation Tests /// /// Verifies that WithAgentService adds an AgentServiceAnnotation to the resource. /// [Fact] public void WithAgentService_ValidService_AddsAnnotation() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act devuiBuilder.WithAgentService(agentService); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .FirstOrDefault(); Assert.NotNull(annotation); Assert.Same(agentService.Resource, annotation.AgentService); } /// /// Verifies that WithAgentService defaults to agent name being the resource name. /// [Fact] public void WithAgentService_NoAgents_DefaultsToResourceNameAsAgent() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act devuiBuilder.WithAgentService(agentService); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); Assert.Single(annotation.Agents); Assert.Equal("writer-agent", annotation.Agents[0].Id); } /// /// Verifies that WithAgentService with explicit agents uses those agents. /// [Fact] public void WithAgentService_WithAgents_UsesProvidedAgents() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "multi-agent-service"); var agents = new[] { new AgentEntityInfo("agent1", "First agent"), new AgentEntityInfo("agent2", "Second agent") }; // Act devuiBuilder.WithAgentService(agentService, agents: agents); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); Assert.Equal(2, annotation.Agents.Count); Assert.Equal("agent1", annotation.Agents[0].Id); Assert.Equal("agent2", annotation.Agents[1].Id); } /// /// Verifies that WithAgentService with custom prefix uses that prefix. /// [Fact] public void WithAgentService_WithEntityIdPrefix_UsesProvidedPrefix() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act devuiBuilder.WithAgentService(agentService, entityIdPrefix: "custom-prefix"); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); Assert.Equal("custom-prefix", annotation.EntityIdPrefix); } /// /// Verifies that WithAgentService without prefix leaves EntityIdPrefix null. /// [Fact] public void WithAgentService_NoEntityIdPrefix_PrefixIsNull() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act devuiBuilder.WithAgentService(agentService); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); Assert.Null(annotation.EntityIdPrefix); } #endregion #region Chaining Tests /// /// Verifies that WithAgentService returns the builder for chaining. /// [Fact] public void WithAgentService_ReturnsSameBuilder_ForChaining() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act var result = devuiBuilder.WithAgentService(agentService); // Assert Assert.Same(devuiBuilder, result); } /// /// Verifies that multiple WithAgentService calls can be chained. /// [Fact] public void WithAgentService_MultipleCalls_AddsMultipleAnnotations() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var writerService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); var editorService = CreateMockAgentServiceBuilder(appBuilder, "editor-agent"); // Act devuiBuilder .WithAgentService(writerService) .WithAgentService(editorService); // Assert var annotations = devuiBuilder.Resource.Annotations .OfType() .ToList(); Assert.Equal(2, annotations.Count); Assert.Contains(annotations, a => a.AgentService.Name == "writer-agent"); Assert.Contains(annotations, a => a.AgentService.Name == "editor-agent"); } /// /// Verifies that AddDevUI returns a builder that can be chained with WithAgentService. /// [Fact] public void AddDevUI_CanChainWithAgentService() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act - Chain AddDevUI with WithAgentService var result = appBuilder.AddDevUI("devui").WithAgentService(agentService); // Assert Assert.NotNull(result); var annotation = result.Resource.Annotations .OfType() .FirstOrDefault(); Assert.NotNull(annotation); } #endregion #region Relationship Tests /// /// Verifies that WithAgentService creates a relationship annotation. /// [Fact] public void WithAgentService_CreatesRelationshipAnnotation() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act devuiBuilder.WithAgentService(agentService); // Assert var relationship = devuiBuilder.Resource.Annotations .OfType() .FirstOrDefault(); Assert.NotNull(relationship); Assert.Equal("agent-backend", relationship.Type); } /// /// Verifies that multiple WithAgentService calls create multiple relationship annotations. /// [Fact] public void WithAgentService_MultipleCalls_CreatesMultipleRelationships() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var writerService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); var editorService = CreateMockAgentServiceBuilder(appBuilder, "editor-agent"); // Act devuiBuilder .WithAgentService(writerService) .WithAgentService(editorService); // Assert var relationships = devuiBuilder.Resource.Annotations .OfType() .ToList(); Assert.Equal(2, relationships.Count); Assert.All(relationships, r => Assert.Equal("agent-backend", r.Type)); } #endregion #region Agent Metadata Tests /// /// Verifies that agent description is preserved when specified. /// [Fact] public void WithAgentService_AgentWithDescription_PreservesDescription() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); var agents = new[] { new AgentEntityInfo("writer", "Writes creative stories") }; // Act devuiBuilder.WithAgentService(agentService, agents: agents); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); Assert.Equal("Writes creative stories", annotation.Agents[0].Description); } /// /// Verifies that custom agent properties are preserved. /// [Fact] public void WithAgentService_CustomAgentProperties_ArePreserved() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "custom-service"); var agents = new[] { new AgentEntityInfo("custom-agent") { Name = "Custom Display Name", Type = "workflow", Framework = "custom_framework" } }; // Act devuiBuilder.WithAgentService(agentService, agents: agents); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); var agent = annotation.Agents[0]; Assert.Equal("custom-agent", agent.Id); Assert.Equal("Custom Display Name", agent.Name); Assert.Equal("workflow", agent.Type); Assert.Equal("custom_framework", agent.Framework); } /// /// Verifies that empty agents array can be explicitly provided and is respected. /// [Fact] public void WithAgentService_EmptyAgentsArray_UsesEmptyArray() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devuiBuilder = appBuilder.AddDevUI("devui"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); var emptyAgents = Array.Empty(); // Act devuiBuilder.WithAgentService(agentService, agents: emptyAgents); // Assert var annotation = devuiBuilder.Resource.Annotations .OfType() .First(); // When explicitly passing an empty array, the extension method respects it // This is the expected behavior - explicit empty means "discover at runtime" Assert.Empty(annotation.Agents); } #endregion #region Edge Case Tests /// /// Verifies that AddDevUI can be called multiple times with different names. /// [Fact] public void AddDevUI_MultipleCalls_CreatesSeparateResources() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); // Act var devui1 = appBuilder.AddDevUI("devui1"); var devui2 = appBuilder.AddDevUI("devui2"); // Assert Assert.NotSame(devui1.Resource, devui2.Resource); Assert.Equal("devui1", devui1.Resource.Name); Assert.Equal("devui2", devui2.Resource.Name); } /// /// Verifies that same agent service can be added to multiple DevUI resources. /// [Fact] public void WithAgentService_SameServiceToMultipleDevUI_Works() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devui1 = appBuilder.AddDevUI("devui1"); var devui2 = appBuilder.AddDevUI("devui2"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "shared-agent"); // Act devui1.WithAgentService(agentService); devui2.WithAgentService(agentService); // Assert var annotation1 = devui1.Resource.Annotations.OfType().Single(); var annotation2 = devui2.Resource.Annotations.OfType().Single(); Assert.Same(annotation1.AgentService, annotation2.AgentService); } /// /// Verifies that WithAgentService works with different entity ID prefixes for the same service. /// [Fact] public void WithAgentService_DifferentPrefixesToDifferentDevUI_Works() { // Arrange var appBuilder = DistributedApplication.CreateBuilder(); var devui1 = appBuilder.AddDevUI("devui1"); var devui2 = appBuilder.AddDevUI("devui2"); var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent"); // Act devui1.WithAgentService(agentService, entityIdPrefix: "prefix1"); devui2.WithAgentService(agentService, entityIdPrefix: "prefix2"); // Assert var annotation1 = devui1.Resource.Annotations.OfType().Single(); var annotation2 = devui2.Resource.Annotations.OfType().Single(); Assert.Equal("prefix1", annotation1.EntityIdPrefix); Assert.Equal("prefix2", annotation2.EntityIdPrefix); } #endregion #region Helper Methods /// /// Creates a mock agent service builder for testing. /// Uses a minimal resource implementation that satisfies IResourceWithEndpoints. /// private static IResourceBuilder CreateMockAgentServiceBuilder( IDistributedApplicationBuilder appBuilder, string name) { // Create a mock resource that implements IResourceWithEndpoints var mockResource = new Mock(); mockResource.Setup(r => r.Name).Returns(name); mockResource.Setup(r => r.Annotations).Returns(new ResourceAnnotationCollection()); var mockBuilder = new Mock>(); mockBuilder.Setup(b => b.Resource).Returns(mockResource.Object); mockBuilder.Setup(b => b.ApplicationBuilder).Returns(appBuilder); return mockBuilder.Object; } #endregion }