From 04af33145a2cded912cd8f43bdf98ac7d1011396 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 16 Jun 2026 14:58:25 -0400 Subject: [PATCH] test(hosting): wait for proxyless EffectivePort before asserting ProxylessContainerCanBeReferenced and WithEndpointProxySupportDisablesProxies ([OuterloopTest], Docker-required) failed every scheduled Outerloop run on the Hosting (8-core-ubuntu-latest) job while passing on Windows: Assert.IsType() Failure: Value is null Expected: typeof(int) Actual: null at ...AssertAllocatedProxylessPort(Service service) line 2319 at ...ProxylessContainerCanBeReferenced() line 1835 Both assert on the redisNoPort proxyless service's Status.EffectivePort. PR #17924 made Aspire pre-assign the proxyless host port synchronously (Service.Spec.Port) and deliberately excluded proxyless services from the startup address-wait in DcpExecutor, since connection strings use the Aspire-assigned Spec.Port immediately. DCP still echoes the bound port back asynchronously via Status.EffectivePort. On a fast Linux agent the test reads the service list before DCP populates EffectivePort, so the assertion sees null; on Windows the update lands first, so it passes. The unit-test fake sets EffectivePort = Spec.Port synchronously, hiding the race. Fix is test-side: wait for the proxyless service to report a non-null EffectivePort (via KubernetesHelper.GetResourceByNameAsync, the existing watch-based waiter) before asserting, instead of reading a once-fetched service list. No product change - the orchestrator behavior is correct. Fixes #8773 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DistributedApplicationTests.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 4cdf75dd632..579c97de408 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -1809,7 +1809,6 @@ public async Task ProxylessContainerCanBeReferenced() await clientA.GetStringAsync("/").DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); var s = app.Services.GetRequiredService(); - var serviceList = await s.ListAsync().DefaultTimeout(); var exeList = await s.ListAsync().DefaultTimeout(); var service = Assert.Single(exeList, c => $"{testName}-servicea".Equals(c.AppModelResourceName)); @@ -1831,7 +1830,8 @@ public async Task ProxylessContainerCanBeReferenced() Assert.Equal(port, Assert.Single(redisContainer.Spec.Ports!).HostPort); } - var otherRedisService = GetEndpointService(serviceList, redisNoPort.Resource, redisNoPort.Resource.PrimaryEndpoint); + var otherRedisService = await WaitForAllocatedProxylessServiceAsync(s, redisNoPort.Resource, redisNoPort.Resource.PrimaryEndpoint) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); var otherRedisPort = AssertAllocatedProxylessPort(otherRedisService); var otherRedisEnv = Assert.Single(service.Spec.Env!, e => e.Name == $"ConnectionStrings__{testName}-redisNoPort"); sslVal = redisNoPort.Resource.TlsEnabled ? ",ssl=true" : string.Empty; @@ -1908,7 +1908,8 @@ public async Task WithEndpointProxySupportDisablesProxies() Assert.Equal(port, Assert.Single(redisContainer.Spec.Ports!).HostPort); } - var otherRedisService = GetEndpointService(serviceList, redisNoPort.Resource, redisNoPort.Resource.PrimaryEndpoint); + var otherRedisService = await WaitForAllocatedProxylessServiceAsync(s, redisNoPort.Resource, redisNoPort.Resource.PrimaryEndpoint) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); var otherRedisPort = AssertAllocatedProxylessPort(otherRedisService); var otherRedisEnv = Assert.Single(service.Spec.Env!, e => e.Name == $"ConnectionStrings__{testName}-redisNoPort"); sslVal = redisNoPort.Resource.TlsEnabled ? ",ssl=true" : string.Empty; @@ -2304,11 +2305,25 @@ private static IResourceBuilder AddRedisContainer(IDistribute .WithImageRegistry(AspireTestContainerRegistry); } - private static Service GetEndpointService(IEnumerable services, RedisResource redis, EndpointReference endpoint) + private static Task WaitForAllocatedProxylessServiceAsync(IKubernetesService kubernetesService, RedisResource redis, EndpointReference endpoint, CancellationToken cancellationToken = default) { var hasMultipleEndpoints = redis.Annotations.OfType().Count() > 1; var expectedServiceName = hasMultipleEndpoints ? $"{redis.Name}-{endpoint.EndpointName}" : redis.Name; - return Assert.Single(services, s => string.Equals(s.Metadata.Name, expectedServiceName, StringComparison.Ordinal)); + + // A proxyless endpoint's host port is assigned synchronously by Aspire (Service.Spec.Port), + // but DCP reports the actually-bound port back asynchronously via Service.Status.EffectivePort. + // Orchestrator startup intentionally does NOT wait for DCP to echo the effective address of + // proxyless services (they are excluded from the startup address-wait in DcpExecutor) because + // connection strings are built from the Aspire-assigned Spec.Port and are usable immediately. + // As a result, a fast agent can observe the Service before DCP has populated EffectivePort, so + // wait until it appears before asserting on it (otherwise the EffectivePort assertion in + // AssertAllocatedProxylessPort races and is null on fast Linux CI agents while passing on Windows). + return KubernetesHelper.GetResourceByNameAsync( + kubernetesService, + expectedServiceName, + string.Empty, + service => service.Status?.EffectivePort is not null, + cancellationToken); } private static int AssertAllocatedProxylessPort(Service service)