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
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Projects/ProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,11 @@ public async Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(F

if (projectFile is not null)
{
if (createSettingsFile)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this true?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true for most user-facing CLI commands that mutate or operate on a project:

┌────────────────────────────┬─────────────┐
│ Command │ Value │
├────────────────────────────┼─────────────┤
│ RunCommand │ true │
├────────────────────────────┼─────────────┤
│ AddCommand │ true │
├────────────────────────────┼─────────────┤
│ UpdateCommand │ true │
├────────────────────────────┼─────────────┤
│ ExecCommand │ true │
├────────────────────────────┼─────────────┤
│ PipelineCommandBase (×2) │ true │
├────────────────────────────┼─────────────┤
│ RestoreCommand │ false │
├────────────────────────────┼─────────────┤
│ AppHostLauncher │ false │
├────────────────────────────┼─────────────┤
│ ExtensionInternalCommand │ false │
├────────────────────────────┼─────────────┤
│ SecretStoreResolver │ false │
└────────────────────────────┴─────────────┘

{
await CreateSettingsFileAsync(projectFile, cancellationToken);
}

return new AppHostProjectSearchResult(projectFile, [projectFile]);
}

Expand Down
50 changes: 50 additions & 0 deletions tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConfigurationService>.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<AspireConfigFile>(configJson);
Assert.NotNull(migratedConfig?.AppHost?.Path);
}

[Fact]
public async Task FindAppHostProjectFilesAsync_DiscoversSingleFileAppHostInRootDirectory()
{
Expand Down
Loading