diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 62bcc6f2dc..33b733c389 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -449,6 +449,7 @@
+
diff --git a/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentEntityInfoTests.cs b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentEntityInfoTests.cs
new file mode 100644
index 0000000000..00f84425d9
--- /dev/null
+++ b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentEntityInfoTests.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Aspire.Hosting.AgentFramework;
+
+namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
+
+///
+/// Unit tests for the record.
+///
+public class AgentEntityInfoTests
+{
+ #region Constructor Tests
+
+ ///
+ /// Verifies that the Id property is set from the constructor parameter.
+ ///
+ [Fact]
+ public void Constructor_WithId_SetsIdProperty()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent");
+
+ // Assert
+ Assert.Equal("test-agent", info.Id);
+ }
+
+ ///
+ /// Verifies that the Description property is set when provided.
+ ///
+ [Fact]
+ public void Constructor_WithDescription_SetsDescriptionProperty()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent", "A test agent");
+
+ // Assert
+ Assert.Equal("A test agent", info.Description);
+ }
+
+ ///
+ /// Verifies that the Description property is null when not provided.
+ ///
+ [Fact]
+ public void Constructor_WithoutDescription_DescriptionIsNull()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent");
+
+ // Assert
+ Assert.Null(info.Description);
+ }
+
+ #endregion
+
+ #region Default Value Tests
+
+ ///
+ /// Verifies that Name defaults to the Id value when not explicitly set.
+ ///
+ [Fact]
+ public void Name_NotSet_DefaultsToId()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent");
+
+ // Assert
+ Assert.Equal("test-agent", info.Name);
+ }
+
+ ///
+ /// Verifies that Name can be overridden with a custom value.
+ ///
+ [Fact]
+ public void Name_Set_ReturnsCustomValue()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent") { Name = "Custom Name" };
+
+ // Assert
+ Assert.Equal("Custom Name", info.Name);
+ }
+
+ ///
+ /// Verifies that Type defaults to "agent".
+ ///
+ [Fact]
+ public void Type_NotSet_DefaultsToAgent()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent");
+
+ // Assert
+ Assert.Equal("agent", info.Type);
+ }
+
+ ///
+ /// Verifies that Type can be overridden with a custom value.
+ ///
+ [Fact]
+ public void Type_Set_ReturnsCustomValue()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent") { Type = "workflow" };
+
+ // Assert
+ Assert.Equal("workflow", info.Type);
+ }
+
+ ///
+ /// Verifies that Framework defaults to "agent_framework".
+ ///
+ [Fact]
+ public void Framework_NotSet_DefaultsToAgentFramework()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent");
+
+ // Assert
+ Assert.Equal("agent_framework", info.Framework);
+ }
+
+ ///
+ /// Verifies that Framework can be overridden with a custom value.
+ ///
+ [Fact]
+ public void Framework_Set_ReturnsCustomValue()
+ {
+ // Arrange & Act
+ var info = new AgentEntityInfo("test-agent") { Framework = "custom_framework" };
+
+ // Assert
+ Assert.Equal("custom_framework", info.Framework);
+ }
+
+ #endregion
+
+ #region Record Equality Tests
+
+ ///
+ /// Verifies that two AgentEntityInfo records with identical values are equal.
+ ///
+ [Fact]
+ public void Equality_SameValues_AreEqual()
+ {
+ // Arrange
+ var info1 = new AgentEntityInfo("agent", "description");
+ var info2 = new AgentEntityInfo("agent", "description");
+
+ // Assert
+ Assert.Equal(info1, info2);
+ }
+
+ ///
+ /// Verifies that two AgentEntityInfo records with different Ids are not equal.
+ ///
+ [Fact]
+ public void Equality_DifferentIds_AreNotEqual()
+ {
+ // Arrange
+ var info1 = new AgentEntityInfo("agent1");
+ var info2 = new AgentEntityInfo("agent2");
+
+ // Assert
+ Assert.NotEqual(info1, info2);
+ }
+
+ ///
+ /// Verifies that with-expression creates a modified copy.
+ ///
+ [Fact]
+ public void WithExpression_ModifiesProperty_CreatesNewInstance()
+ {
+ // Arrange
+ var original = new AgentEntityInfo("agent", "Original description");
+
+ // Act
+ var modified = original with { Description = "Modified description" };
+
+ // Assert
+ Assert.Equal("Original description", original.Description);
+ Assert.Equal("Modified description", modified.Description);
+ Assert.Equal(original.Id, modified.Id);
+ }
+
+ #endregion
+}
diff --git a/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentFrameworkBuilderExtensionsTests.cs b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentFrameworkBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..e9af778ac4
--- /dev/null
+++ b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentFrameworkBuilderExtensionsTests.cs
@@ -0,0 +1,568 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using Aspire.Hosting.AgentFramework;
+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
+}
diff --git a/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentServiceAnnotationTests.cs b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentServiceAnnotationTests.cs
new file mode 100644
index 0000000000..ee5180a914
--- /dev/null
+++ b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/AgentServiceAnnotationTests.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Aspire.Hosting.AgentFramework;
+using Aspire.Hosting.ApplicationModel;
+using Moq;
+
+namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
+
+///
+/// Unit tests for the class.
+///
+public class AgentServiceAnnotationTests
+{
+ #region Constructor Validation Tests
+
+ ///
+ /// Verifies that passing null for agentService throws ArgumentNullException.
+ ///
+ [Fact]
+ public void Constructor_NullAgentService_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ Assert.Throws(() => new AgentServiceAnnotation(null!));
+ }
+
+ ///
+ /// Verifies that a valid agentService can be used to create the annotation.
+ ///
+ [Fact]
+ public void Constructor_ValidAgentService_CreatesAnnotation()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("test-service");
+
+ // Act
+ var annotation = new AgentServiceAnnotation(mockResource.Object);
+
+ // Assert
+ Assert.NotNull(annotation);
+ Assert.Same(mockResource.Object, annotation.AgentService);
+ }
+
+ #endregion
+
+ #region Property Tests
+
+ ///
+ /// Verifies that AgentService property returns the value passed to constructor.
+ ///
+ [Fact]
+ public void AgentService_ReturnsConstructorValue()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("my-service");
+
+ // Act
+ var annotation = new AgentServiceAnnotation(mockResource.Object);
+
+ // Assert
+ Assert.Same(mockResource.Object, annotation.AgentService);
+ }
+
+ ///
+ /// Verifies that EntityIdPrefix returns null when not specified.
+ ///
+ [Fact]
+ public void EntityIdPrefix_NotSpecified_ReturnsNull()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("test-service");
+
+ // Act
+ var annotation = new AgentServiceAnnotation(mockResource.Object);
+
+ // Assert
+ Assert.Null(annotation.EntityIdPrefix);
+ }
+
+ ///
+ /// Verifies that EntityIdPrefix returns the value passed to constructor.
+ ///
+ [Fact]
+ public void EntityIdPrefix_Specified_ReturnsValue()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("test-service");
+
+ // Act
+ var annotation = new AgentServiceAnnotation(mockResource.Object, entityIdPrefix: "custom-prefix");
+
+ // Assert
+ Assert.Equal("custom-prefix", annotation.EntityIdPrefix);
+ }
+
+ ///
+ /// Verifies that Agents returns empty collection when not specified.
+ ///
+ [Fact]
+ public void Agents_NotSpecified_ReturnsEmptyCollection()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("test-service");
+
+ // Act
+ var annotation = new AgentServiceAnnotation(mockResource.Object);
+
+ // Assert
+ Assert.NotNull(annotation.Agents);
+ Assert.Empty(annotation.Agents);
+ }
+
+ ///
+ /// Verifies that Agents returns the list passed to constructor.
+ ///
+ [Fact]
+ public void Agents_Specified_ReturnsValue()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("test-service");
+ var agents = new[] { new AgentEntityInfo("agent1"), new AgentEntityInfo("agent2") };
+
+ // Act
+ var annotation = new AgentServiceAnnotation(mockResource.Object, agents: agents);
+
+ // Assert
+ Assert.Equal(2, annotation.Agents.Count);
+ Assert.Equal("agent1", annotation.Agents[0].Id);
+ Assert.Equal("agent2", annotation.Agents[1].Id);
+ }
+
+ #endregion
+
+ #region Full Constructor Tests
+
+ ///
+ /// Verifies that all constructor parameters are correctly stored.
+ ///
+ [Fact]
+ public void Constructor_AllParameters_SetsAllProperties()
+ {
+ // Arrange
+ var mockResource = new Mock();
+ mockResource.Setup(r => r.Name).Returns("full-service");
+ var agents = new[] { new AgentEntityInfo("writer", "Writes stories") };
+
+ // Act
+ var annotation = new AgentServiceAnnotation(
+ mockResource.Object,
+ entityIdPrefix: "writer-backend",
+ agents: agents);
+
+ // Assert
+ Assert.Same(mockResource.Object, annotation.AgentService);
+ Assert.Equal("writer-backend", annotation.EntityIdPrefix);
+ Assert.Single(annotation.Agents);
+ Assert.Equal("writer", annotation.Agents[0].Id);
+ Assert.Equal("Writes stories", annotation.Agents[0].Description);
+ }
+
+ #endregion
+}
diff --git a/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj
new file mode 100644
index 0000000000..ab2871717b
--- /dev/null
+++ b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/DevUIResourceTests.cs b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/DevUIResourceTests.cs
new file mode 100644
index 0000000000..0f66286ae2
--- /dev/null
+++ b/dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/DevUIResourceTests.cs
@@ -0,0 +1,215 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Net.Sockets;
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
+
+///
+/// Unit tests for the class.
+///
+public class DevUIResourceTests
+{
+ #region Constructor Tests
+
+ ///
+ /// Verifies that the resource name is correctly set.
+ ///
+ [Fact]
+ public void Constructor_WithName_SetsName()
+ {
+ // Arrange & Act
+ var resource = new DevUIResource("test-devui");
+
+ // Assert
+ Assert.Equal("test-devui", resource.Name);
+ }
+
+ ///
+ /// Verifies that the resource implements IResourceWithEndpoints.
+ ///
+ [Fact]
+ public void Resource_ImplementsIResourceWithEndpoints()
+ {
+ // Arrange & Act
+ var resource = new DevUIResource("test-devui");
+
+ // Assert
+ Assert.IsAssignableFrom(resource);
+ }
+
+ ///
+ /// Verifies that the resource implements IResourceWithWaitSupport.
+ ///
+ [Fact]
+ public void Resource_ImplementsIResourceWithWaitSupport()
+ {
+ // Arrange & Act
+ var resource = new DevUIResource("test-devui");
+
+ // Assert
+ Assert.IsAssignableFrom(resource);
+ }
+
+ #endregion
+
+ #region Endpoint Annotation Tests
+
+ ///
+ /// Verifies that the resource has an HTTP endpoint annotation when port is specified.
+ ///
+ [Fact]
+ public void Constructor_WithPort_AddsEndpointAnnotation()
+ {
+ // Arrange & Act
+ var resource = CreateResourceWithPort(8090);
+
+ // Assert
+ var endpoint = resource.Annotations.OfType().FirstOrDefault();
+ Assert.NotNull(endpoint);
+ Assert.Equal("http", endpoint.Name);
+ Assert.Equal(8090, endpoint.Port);
+ }
+
+ ///
+ /// Verifies that the endpoint annotation has correct protocol type.
+ ///
+ [Fact]
+ public void EndpointAnnotation_HasTcpProtocol()
+ {
+ // Arrange
+ var resource = CreateResourceWithPort(8080);
+
+ // Act
+ var endpoint = resource.Annotations.OfType().First();
+
+ // Assert
+ Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
+ }
+
+ ///
+ /// Verifies that the endpoint annotation has HTTP URI scheme.
+ ///
+ [Fact]
+ public void EndpointAnnotation_HasHttpUriScheme()
+ {
+ // Arrange
+ var resource = CreateResourceWithPort(8080);
+
+ // Act
+ var endpoint = resource.Annotations.OfType().First();
+
+ // Assert
+ Assert.Equal("http", endpoint.UriScheme);
+ }
+
+ ///
+ /// Verifies that the endpoint is not proxied.
+ ///
+ [Fact]
+ public void EndpointAnnotation_IsNotProxied()
+ {
+ // Arrange
+ var resource = CreateResourceWithPort(8080);
+
+ // Act
+ var endpoint = resource.Annotations.OfType().First();
+
+ // Assert
+ Assert.False(endpoint.IsProxied);
+ }
+
+ ///
+ /// Verifies that the endpoint target host is localhost.
+ ///
+ [Fact]
+ public void EndpointAnnotation_TargetHostIsLocalhost()
+ {
+ // Arrange
+ var resource = CreateResourceWithPort(8080);
+
+ // Act
+ var endpoint = resource.Annotations.OfType().First();
+
+ // Assert
+ Assert.Equal("localhost", endpoint.TargetHost);
+ }
+
+ ///
+ /// Verifies that the endpoint has no fixed port when null is passed.
+ ///
+ [Fact]
+ public void Constructor_WithNullPort_EndpointHasNullPort()
+ {
+ // Arrange & Act
+ var resource = CreateResourceWithPort(null);
+
+ // Assert
+ var endpoint = resource.Annotations.OfType().FirstOrDefault();
+ Assert.NotNull(endpoint);
+ Assert.Null(endpoint.Port);
+ }
+
+ #endregion
+
+ #region PrimaryEndpoint Tests
+
+ ///
+ /// Verifies that PrimaryEndpoint returns an endpoint reference.
+ ///
+ [Fact]
+ public void PrimaryEndpoint_ReturnsEndpointReference()
+ {
+ // Arrange
+ var resource = CreateResourceWithPort(8080);
+
+ // Act
+ var endpoint = resource.PrimaryEndpoint;
+
+ // Assert
+ Assert.NotNull(endpoint);
+ Assert.Same(resource, endpoint.Resource);
+ }
+
+ ///
+ /// Verifies that PrimaryEndpoint returns the same instance on multiple calls.
+ ///
+ [Fact]
+ public void PrimaryEndpoint_MultipleCalls_ReturnsSameInstance()
+ {
+ // Arrange
+ var resource = CreateResourceWithPort(8080);
+
+ // Act
+ var endpoint1 = resource.PrimaryEndpoint;
+ var endpoint2 = resource.PrimaryEndpoint;
+
+ // Assert
+ Assert.Same(endpoint1, endpoint2);
+ }
+
+ #endregion
+
+ ///
+ /// Creates a DevUIResource using reflection to access the internal constructor.
+ /// This is necessary because the (name, port) constructor is internal.
+ ///
+ private static DevUIResource CreateResourceWithPort(int? port)
+ {
+ // Use reflection to call the internal constructor
+ var constructorInfo = typeof(DevUIResource).GetConstructor(
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
+ null,
+ [typeof(string), typeof(int?)],
+ null);
+
+ if (constructorInfo is null)
+ {
+ throw new InvalidOperationException("Could not find internal DevUIResource constructor");
+ }
+
+ return (DevUIResource)constructorInfo.Invoke(["test-devui", port]);
+ }
+}