From 89471c2748799fe4a0f1aaff2bd7cbc08d4daaee Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 24 Mar 2026 19:37:55 +1100 Subject: [PATCH 1/2] Fix migration from .aspire/settings.json to aspire.config.json When the AppHost was found from legacy .aspire/settings.json, the UseOrFindAppHostProjectFileAsync method returned early without calling CreateSettingsFileAsync, so the migration to aspire.config.json never triggered. This fix ensures CreateSettingsFileAsync is called when createSettingsFile is true and the AppHost is found from settings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Projects/ProjectLocator.cs | 5 +++ .../Projects/ProjectLocatorTests.cs | 43 +++++++++++++++++++ 2 files changed, 48 insertions(+) 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..9276592a122 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs @@ -316,6 +316,49 @@ 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"); + + 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(workspace.WorkspaceRoot.FullName, appHostProjectFile.FullName); + 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 + 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() { From f5467fc9a5d1db043c583c8d310670a2721aac80 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 24 Mar 2026 21:52:40 +1100 Subject: [PATCH 2/2] Fix migration test to use correct relative path Address Copilot review feedback: compute appHostPath relative to .aspire/ directory (not workspace root), normalize to forward slashes, and add a decoy project to ensure the legacy-settings resolution path is actually exercised rather than falling through to directory scanning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs b/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs index 9276592a122..108dc42e90e 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs @@ -326,13 +326,20 @@ public async Task UseOrFindAppHostProjectFile_MigratesLegacySettingsToAspireConf 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(workspace.WorkspaceRoot.FullName, appHostProjectFile.FullName); + 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 @@ -346,7 +353,7 @@ public async Task UseOrFindAppHostProjectFile_MigratesLegacySettingsToAspireConf // Act var foundAppHost = await locator.UseOrFindAppHostProjectFileAsync(null, createSettingsFile: true, CancellationToken.None).DefaultTimeout(); - // Assert: correct AppHost was found + // Assert: correct AppHost was found (not the decoy, proving legacy settings were used) Assert.NotNull(foundAppHost); Assert.Equal(appHostProjectFile.FullName, foundAppHost.FullName);