| File: TypeScriptCodegenValidationTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.Cli.Tests.Utils; using Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; /// <summary> /// End-to-end test that validates the <c>aspire restore</c> command by creating a /// TypeScript AppHost with two integrations and verifying the generated SDK files /// are produced in the <c>.modules/</c> directory. /// </summary> public sealed class TypeScriptCodegenValidationTests(ITestOutputHelper output) { [Fact] public async Task RestoreGeneratesSdkFiles() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliInDockerAsync(installMode, counter); // Step 1: Create a TypeScript AppHost await auto.TypeAsync("aspire init --language typescript --non-interactive"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2)); await auto.WaitForSuccessPromptAsync(counter); // Step 2: Add two integrations await auto.TypeAsync("aspire add Aspire.Hosting.Redis"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("The package Aspire.Hosting.", timeout: TimeSpan.FromMinutes(2)); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("aspire add Aspire.Hosting.SqlServer"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("The package Aspire.Hosting.", timeout: TimeSpan.FromMinutes(2)); await auto.WaitForSuccessPromptAsync(counter); // Step 3: Run aspire restore and verify success await auto.TypeAsync("aspire restore"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("SDK code restored successfully", timeout: TimeSpan.FromMinutes(3)); await auto.WaitForSuccessPromptAsync(counter); // Step 4: Verify generated SDK files exist var modulesDir = Path.Combine(workspace.WorkspaceRoot.FullName, ".modules"); if (!Directory.Exists(modulesDir)) { throw new InvalidOperationException($".modules directory was not created at {modulesDir}"); } var expectedFiles = new[] { "aspire.ts", "base.ts", "transport.ts" }; foreach (var file in expectedFiles) { var filePath = Path.Combine(modulesDir, file); if (!File.Exists(filePath)) { throw new InvalidOperationException($"Expected generated file not found: {filePath}"); } var content = File.ReadAllText(filePath); if (string.IsNullOrWhiteSpace(content)) { throw new InvalidOperationException($"Generated file is empty: {filePath}"); } } // Verify aspire.ts contains symbols from both integrations var aspireTs = File.ReadAllText(Path.Combine(modulesDir, "aspire.ts")); if (!aspireTs.Contains("addRedis")) { throw new InvalidOperationException("aspire.ts does not contain addRedis from Aspire.Hosting.Redis"); } if (!aspireTs.Contains("addSqlServer")) { throw new InvalidOperationException("aspire.ts does not contain addSqlServer from Aspire.Hosting.SqlServer"); } await auto.TypeAsync("exit"); await auto.EnterAsync(); await pendingRun; } [Fact] public async Task RunWithMissingAwaitShowsHelpfulError() { using var workspace = TemporaryWorkspace.Create(output); var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); var isCI = CliE2ETestHelpers.IsRunningInCI; using var terminal = CliE2ETestHelpers.CreateTestTerminal(); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); // PrepareEnvironment await auto.PrepareEnvironmentAsync(workspace, counter); if (isCI) { await auto.InstallAspireBundleFromPullRequestAsync(prNumber, counter); await auto.SourceAspireBundleEnvironmentAsync(counter); await auto.VerifyAspireCliVersionAsync(commitSha, counter); } await auto.TypeAsync("aspire init --language typescript --non-interactive"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2)); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("aspire add Aspire.Hosting.PostgreSQL"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("The package Aspire.Hosting.", timeout: TimeSpan.FromMinutes(2)); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("aspire restore"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("SDK code restored successfully", timeout: TimeSpan.FromMinutes(3)); await auto.WaitForSuccessPromptAsync(counter); var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); var newContent = """ import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); const postgres = builder.addPostgres("postgres"); const db = postgres.addDatabase("db"); await builder.addContainer("consumer", "nginx") .withReference(db); await builder.build().run(); """; File.WriteAllText(appHostPath, newContent); await auto.TypeAsync("aspire run"); await auto.EnterAsync(); await auto.WaitUntilAsync(s => s.ContainsText("❌ AppHost Error:") && s.ContainsText("Did you forget 'await'"), timeout: TimeSpan.FromMinutes(3), description: "waiting for AppHost error with await hint"); await auto.WaitForAnyPromptAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); await pendingRun; } }