Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions tests/Aspire.Cli.EndToEnd.Tests/NewWithAgentInitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public async Task AspireNew_WithAgentInit_InstallsPlaywrightWithoutErrors()

// Run aspire new with the Starter template, going through all prompts manually
// so we can ACCEPT the agent init prompt instead of declining it.
await auto.TypeAsync("aspire new");
// Pass --skill-locations and --skills as CLI flags so the test does not depend on
// the position or count of entries in the interactive skill-selection menus, which
// change whenever the bundle ships a new skill.
await auto.TypeAsync("aspire new --skill-locations claudecode --skills playwright-cli");
await auto.EnterAsync();

// Template selection: accept default Starter App
Expand Down Expand Up @@ -107,32 +110,9 @@ await auto.WaitUntilAsync(
await auto.WaitAsync(500);
await auto.TypeAsync("y");

// Agent init: skill location - select Claude Code
await auto.WaitUntilAsync(
s => s.ContainsText("skill files be installed"),
timeout: TimeSpan.FromSeconds(60),
description: "skill location prompt");
await auto.TypeAsync(" "); // Toggle off default Standard location
await auto.DownAsync();
await auto.TypeAsync(" "); // Toggle on Claude Code location
await auto.EnterAsync();

// Agent init: skill selection - this test validates Playwright acquisition,
// so deselect the default Aspire bundle skills and select only Playwright CLI.
await auto.WaitUntilAsync(
s => s.ContainsText("skills should be installed"),
timeout: TimeSpan.FromSeconds(30),
description: "skill selection prompt");
await auto.TypeAsync(" "); // Toggle off Aspire
await auto.DownAsync();
await auto.TypeAsync(" "); // Toggle off aspireify
await auto.DownAsync();
await auto.TypeAsync(" "); // Toggle off aspire-deployment
await auto.DownAsync();
await auto.TypeAsync(" "); // Toggle on Playwright CLI
await auto.EnterAsync();

// Wait for agent init to complete (downloads @playwright/cli from npm).
// Skill location and skill selection are provided via --skill-locations/--skills flags
// on the aspire new invocation above, so no interactive navigation is needed here.
// Fail the test immediately if a provenance verification error appears.
await auto.WaitUntilAsync(s =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public sealed class BrowserTokenDashboardServerWithHttpAndHttpsFixture : Dashboa
{
public BrowserTokenDashboardServerWithHttpAndHttpsFixture()
{
Configuration[DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "https://localhost:0;http://localhost:0";
// Bind to 127.0.0.1 rather than the "localhost" hostname: Kestrel rejects dynamic-port
// binding (":0") on "localhost" with "Dynamic port binding is not supported when binding
// to localhost. You must either bind to 127.0.0.1:0 or [::1]:0, or both." The WebKit test
// navigates to the resolved address with IgnoreHTTPSErrors, so the loopback IP is fine.
Configuration[DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "https://127.0.0.1:0;http://127.0.0.1:0";
Configuration[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = nameof(FrontendAuthMode.BrowserToken);
Configuration[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = "VALID_TOKEN";
}
Expand Down
25 changes: 20 additions & 5 deletions tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,6 @@ public async Task ProxylessContainerCanBeReferenced()
await clientA.GetStringAsync("/").DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout);

var s = app.Services.GetRequiredService<IKubernetesService>();
var serviceList = await s.ListAsync<Service>().DefaultTimeout();
var exeList = await s.ListAsync<Executable>().DefaultTimeout();

var service = Assert.Single(exeList, c => $"{testName}-servicea".Equals(c.AppModelResourceName));
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2304,11 +2305,25 @@ private static IResourceBuilder<ContainerResource> AddRedisContainer(IDistribute
.WithImageRegistry(AspireTestContainerRegistry);
}

private static Service GetEndpointService(IEnumerable<Service> services, RedisResource redis, EndpointReference endpoint)
private static Task<Service> WaitForAllocatedProxylessServiceAsync(IKubernetesService kubernetesService, RedisResource redis, EndpointReference endpoint, CancellationToken cancellationToken = default)
{
var hasMultipleEndpoints = redis.Annotations.OfType<EndpointAnnotation>().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<Service>(
kubernetesService,
expectedServiceName,
string.Empty,
service => service.Status?.EffectivePort is not null,
cancellationToken);
}

private static int AssertAllocatedProxylessPort(Service service)
Expand Down
Loading