diff --git a/src/dnvm/PruneCommand.cs b/src/dnvm/PruneCommand.cs index 08da401..8d0d83d 100644 --- a/src/dnvm/PruneCommand.cs +++ b/src/dnvm/PruneCommand.cs @@ -48,12 +48,25 @@ public static async Task Run(DnvmEnv env, Logger logger, Options options) } else { + // Check if the SDK is actually in InstalledSdks. It may not be if the manifest + // was corrupted by a bug fixed in https://github.com/dn-vm/dnvm/pull/274. + // In that case, we just need to clean up the stale entry from RegisteredChannels. + if (!manifest.IsSdkInstalled(sdk.Version, sdk.Dir)) + { + env.Console.Warn($"SDK {sdk.Version} was not found in installed SDKs, cleaning up stale manifest entry."); + manifest = RemoveSdkFromChannels(manifest, sdk.Version); + await @lock.WriteManifest(env, manifest); + continue; + } + Console.WriteLine($"Removing {sdk}"); int result = await UninstallCommand.Run(@lock, env, logger, sdk.Version, sdk.Dir); if (result != 0) { return result; } + // Re-read manifest after uninstall since it was modified + manifest = await @lock.ReadManifest(env); } } return 0; @@ -99,4 +112,22 @@ public static async Task Run(DnvmEnv env, Logger logger, Options options) return sdksToRemove; } + + /// + /// Removes an SDK version from all RegisteredChannels.InstalledSdkVersions. + /// This is used to clean up stale entries left by a bug fixed in + /// https://github.com/dn-vm/dnvm/pull/274. + /// + private static Manifest RemoveSdkFromChannels(Manifest manifest, SemVersion sdkVersion) + { + var updatedChannels = manifest.RegisteredChannels.Select(channel => + { + var updatedInstalledVersions = channel.InstalledSdkVersions + .Where(version => version != sdkVersion) + .ToEq(); + return channel with { InstalledSdkVersions = updatedInstalledVersions }; + }).ToEq(); + + return manifest with { RegisteredChannels = updatedChannels }; + } } \ No newline at end of file diff --git a/test/UnitTests/PruneTests.cs b/test/UnitTests/PruneTests.cs index 79df449..c5f131f 100644 --- a/test/UnitTests/PruneTests.cs +++ b/test/UnitTests/PruneTests.cs @@ -177,4 +177,65 @@ public Task MissingDirectoriesHandled() => RunWithServer(async (server, env) => Assert.Single(finalManifest.InstalledSdks); Assert.Equal(upgradeVersion, finalManifest.InstalledSdks[0].SdkVersion); }); + + /// + /// Tests the scenario from https://github.com/dn-vm/dnvm/issues/311: + /// Before PR #274, uninstalling an SDK removed it from InstalledSdks but not from + /// RegisteredChannels.InstalledSdkVersions. Prune should handle this gracefully by + /// logging a warning and cleaning up the stale manifest entry. + /// + [Fact] + public Task PruneHandlesCorruptedManifestFromPre274Uninstall() => RunWithServer(async (server, env) => + { + var oldVersion = SemVersion.Parse("8.0.100", SemVersionStyles.Strict); + var newVersion = SemVersion.Parse("8.0.101", SemVersionStyles.Strict); + Channel channel = new Channel.Latest(); + + // Create a corrupted manifest that simulates the state left by a pre-PR#274 uninstall: + // - The old SDK version is in RegisteredChannels.InstalledSdkVersions (was not removed) + // - But the old SDK is NOT in InstalledSdks (was removed) + // - The new SDK is properly tracked in both places + var corruptedManifest = Manifest.Empty with + { + InstalledSdks = [ + // Only the new version is in InstalledSdks (old version was uninstalled) + new InstalledSdk + { + SdkVersion = newVersion, + RuntimeVersion = newVersion, + AspNetVersion = newVersion, + ReleaseVersion = newVersion, + SdkDirName = DnvmEnv.DefaultSdkDirName + } + ], + RegisteredChannels = [ + new RegisteredChannel + { + ChannelName = channel, + SdkDirName = DnvmEnv.DefaultSdkDirName, + // Bug: old version is still in InstalledSdkVersions even though it was uninstalled + InstalledSdkVersions = [oldVersion, newVersion] + } + ] + }; + + await Manifest.WriteManifestUnsafe(env, corruptedManifest); + + var console = (TestConsole)env.Console; + var initialOutput = console.Output; + + // Prune should succeed and clean up the stale manifest entry + var pruneResult = await PruneCommand.Run(env, _logger, new PruneCommand.Options()); + + var output = console.Output[initialOutput.Length..]; + + Assert.Equal(0, pruneResult); + Assert.Contains("was not found in installed SDKs", output); + + // Verify the manifest was cleaned up - old version should be removed from channel + var finalManifest = await Manifest.ReadManifestUnsafe(env); + var registeredChannel = Assert.Single(finalManifest.RegisteredChannels); + Assert.Single(registeredChannel.InstalledSdkVersions); + Assert.Equal(newVersion, registeredChannel.InstalledSdkVersions[0]); + }); } \ No newline at end of file