diff --git a/src/Aspire.Cli/Projects/ProjectLocator.cs b/src/Aspire.Cli/Projects/ProjectLocator.cs index 889b5f7aa1c..aa1f6dc5ee6 100644 --- a/src/Aspire.Cli/Projects/ProjectLocator.cs +++ b/src/Aspire.Cli/Projects/ProjectLocator.cs @@ -347,6 +347,11 @@ public async Task UseOrFindAppHostProjectFileAsync(F if (projectFile is not null) { + if (createSettingsFile) + { + await CreateSettingsFileAsync(projectFile, cancellationToken); + } + return new AppHostProjectSearchResult(projectFile, [projectFile]); } diff --git a/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs b/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs index 9003a67c8f4..108dc42e90e 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs @@ -316,6 +316,56 @@ public async Task CreateSettingsFileIfNotExistsAsync_UsesForwardSlashPathSeparat Assert.Contains('/', settings.AppHost.Path); // Ensure forward slashes } + [Fact] + public async Task UseOrFindAppHostProjectFile_MigratesLegacySettingsToAspireConfigJson() + { + // Arrange: create a workspace with a legacy .aspire/settings.json + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var appHostDirectory = workspace.WorkspaceRoot.CreateSubdirectory("MyAppHost"); + var appHostProjectFile = new FileInfo(Path.Combine(appHostDirectory.FullName, "MyAppHost.csproj")); + await File.WriteAllTextAsync(appHostProjectFile.FullName, "Not a real apphost"); + + // Add a decoy project so that a directory scan fallback would have another candidate + var decoyAppHostDirectory = workspace.WorkspaceRoot.CreateSubdirectory("DecoyAppHost"); + var decoyAppHostProjectFile = new FileInfo(Path.Combine(decoyAppHostDirectory.FullName, "DecoyAppHost.csproj")); + await File.WriteAllTextAsync(decoyAppHostProjectFile.FullName, "Not a real apphost"); + + var executionContext = CreateExecutionContext(workspace.WorkspaceRoot); + + // Write legacy .aspire/settings.json with a valid appHostPath + // (CreateExecutionContext already created the .aspire directory) + var aspireSettingsDir = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire")); + var aspireSettingsFile = new FileInfo(Path.Combine(aspireSettingsDir.FullName, "settings.json")); + var relativeAppHostPath = Path + .GetRelativePath(aspireSettingsDir.FullName, appHostProjectFile.FullName) + .Replace(Path.DirectorySeparatorChar, '/'); + await File.WriteAllTextAsync(aspireSettingsFile.FullName, JsonSerializer.Serialize(new { appHostPath = relativeAppHostPath })); + + // Use real ConfigurationService so migration actually writes to disk + var globalSettingsFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "settings.global.json"); + var globalSettingsFile = new FileInfo(globalSettingsFilePath); + var config = new ConfigurationBuilder().Build(); + var configurationService = new ConfigurationService(config, executionContext, globalSettingsFile, NullLogger.Instance); + + var locator = CreateProjectLocator(executionContext, configurationService: configurationService); + + // Act + var foundAppHost = await locator.UseOrFindAppHostProjectFileAsync(null, createSettingsFile: true, CancellationToken.None).DefaultTimeout(); + + // Assert: correct AppHost was found (not the decoy, proving legacy settings were used) + Assert.NotNull(foundAppHost); + Assert.Equal(appHostProjectFile.FullName, foundAppHost.FullName); + + // Assert: aspire.config.json was created by migration + var aspireConfigFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName); + Assert.True(File.Exists(aspireConfigFilePath), "aspire.config.json should have been created by migration from .aspire/settings.json"); + + var configJson = await File.ReadAllTextAsync(aspireConfigFilePath); + var migratedConfig = JsonSerializer.Deserialize(configJson); + Assert.NotNull(migratedConfig?.AppHost?.Path); + } + [Fact] public async Task FindAppHostProjectFilesAsync_DiscoversSingleFileAppHostInRootDirectory() {