/* +------------------------------------------------------------------+ | Author: Ivan Murzak (https://github.com/IvanMurzak) | | Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) | | Copyright (c) 2025 Ivan Murzak | | Licensed under the Apache License, Version 2.0. | | See the LICENSE file in the project root for more information. | +------------------------------------------------------------------+ */ #nullable enable using System.IO; using NUnit.Framework; using com.IvanMurzak.Unity.MCP.Editor.DependencyResolver; namespace com.IvanMurzak.Unity.MCP.Editor.Tests.DependencyResolverTests { /// /// Regression coverage for issue #703 (stale-sibling cleanup) ported to the /// post-#733 flat-layout install model. The on-disk layout is now /// {installPath}/{stem}.{packageVersion}.dll with a /// .nuget-installed.json manifest at the root that maps package IDs /// to their owned DLLs and the version those DLLs were installed at. /// drives /// stale-version removal off that manifest; these tests exercise it /// directly against a temp directory. /// [TestFixture] public class NuGetPackageInstallerStaleVersionCleanupTests { string _installPath = string.Empty; [SetUp] public void SetUp() { _installPath = Path.Combine( Path.GetTempPath(), "UnityMcp-NuGetInstaller-" + Path.GetRandomFileName()); Directory.CreateDirectory(_installPath); } [TearDown] public void TearDown() { if (Directory.Exists(_installPath)) { try { Directory.Delete(_installPath, recursive: true); } catch { /* best-effort cleanup */ } } } [Test] public void RemoveStaleSiblingVersions_DeletesPriorVersionDllAndItsMeta() { // Issue #703 scenario, ported to flat layout: ReflectorNet 5.0.0 is // recorded in the manifest from a previous session, the user upgraded // the Unity package, the new dep graph resolves ReflectorNet 5.1.1. CreateFlatPackage("com.IvanMurzak.ReflectorNet", "5.0.0", "ReflectorNet.5.0.0.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsTrue(removed, "Helper must report that it removed something."); Assert.IsFalse(File.Exists(Path.Combine(_installPath, "ReflectorNet.5.0.0.dll")), "Stale versioned DLL must be removed."); Assert.IsFalse(File.Exists(Path.Combine(_installPath, "ReflectorNet.5.0.0.dll.meta")), "Stale .meta sidecar must be removed alongside its DLL."); // Manifest must reflect the removal. var manifest = NuGetInstallManifest.Load(_installPath); Assert.IsFalse(manifest.Packages.ContainsKey("com.IvanMurzak.ReflectorNet")); } [Test] public void RemoveStaleSiblingVersions_PreservesSameVersionEntry() { // Idempotency: re-running the resolver on an up-to-date project must NOT delete // the entry at the configured version. Otherwise every restore would force // a needless re-extraction. CreateFlatPackage("com.IvanMurzak.ReflectorNet", "5.1.1", "ReflectorNet.5.1.1.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsFalse(removed); Assert.IsTrue(File.Exists(Path.Combine(_installPath, "ReflectorNet.5.1.1.dll"))); Assert.IsTrue(File.Exists(Path.Combine(_installPath, "ReflectorNet.5.1.1.dll.meta"))); } [Test] public void RemoveStaleSiblingVersions_PreservesOtherPackages() { // The scan must not touch entries belonging to a different package Id. CreateFlatPackage("com.IvanMurzak.ReflectorNet", "5.0.0", "ReflectorNet.5.0.0.dll"); CreateFlatPackage("System.Text.Json", "8.0.5", "System.Text.Json.8.0.5.dll"); CreateFlatPackage("Microsoft.AspNetCore.SignalR.Client", "8.0.15", "Microsoft.AspNetCore.SignalR.Client.8.0.15.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsTrue(removed); var manifest = NuGetInstallManifest.Load(_installPath); Assert.IsFalse(manifest.Packages.ContainsKey("com.IvanMurzak.ReflectorNet")); Assert.IsTrue(manifest.Packages.ContainsKey("System.Text.Json")); Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.AspNetCore.SignalR.Client"), "Package with dots in its Id must not be matched against a different package's Id."); } [Test] public void RemoveStaleSiblingVersions_HandlesPackageIdsWithDots() { CreateFlatPackage("Microsoft.AspNetCore.SignalR.Common", "10.0.3", "Microsoft.AspNetCore.SignalR.Common.10.0.3.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "Microsoft.AspNetCore.SignalR.Common", "8.0.15"); Assert.IsTrue(removed); var manifest = NuGetInstallManifest.Load(_installPath); Assert.IsFalse(manifest.Packages.ContainsKey("Microsoft.AspNetCore.SignalR.Common")); } [Test] public void RemoveStaleSiblingVersions_DeletesHigherVersionTooWhenItIsNotTheKeepVersion() { // The cleanup contract is "delete everything that is not the keepVersion", not // "delete only lower versions". CreateFlatPackage("com.IvanMurzak.ReflectorNet", "6.0.0", "ReflectorNet.6.0.0.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsTrue(removed); Assert.IsFalse(File.Exists(Path.Combine(_installPath, "ReflectorNet.6.0.0.dll"))); } [Test] public void RemoveStaleSiblingVersions_ReturnsFalse_WhenInstallPathDoesNotExist() { // First-ever restore on a brand-new project: the install dir is created lazily // by the restorer. The helper must not crash and must report no work done. var doesNotExist = Path.Combine(_installPath, "does-not-exist"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( doesNotExist, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsFalse(removed); } [Test] public void RemoveStaleSiblingVersions_ReturnsFalse_WhenManifestHasNoMatchingEntry() { CreateFlatPackage("System.Text.Json", "8.0.5", "System.Text.Json.8.0.5.dll"); CreateFlatPackage("Microsoft.AspNetCore.SignalR.Client", "8.0.15", "Microsoft.AspNetCore.SignalR.Client.8.0.15.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsFalse(removed); var manifest = NuGetInstallManifest.Load(_installPath); Assert.IsTrue(manifest.Packages.ContainsKey("System.Text.Json")); Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.AspNetCore.SignalR.Client")); } [Test] public void RemoveStaleSiblingVersions_MatchesPackageIdCaseInsensitively() { // Comparisons throughout the resolver are case-insensitive on the package ID. CreateFlatPackage("com.ivanmurzak.reflectornet", "5.0.0", "ReflectorNet.5.0.0.dll"); var removed = NuGetPackageInstaller.RemoveStaleSiblingVersions( _installPath, "com.IvanMurzak.ReflectorNet", "5.1.1"); Assert.IsTrue(removed); var manifest = NuGetInstallManifest.Load(_installPath); Assert.IsFalse(manifest.Packages.ContainsKey("com.ivanmurzak.reflectornet")); } /// /// Creates a fake flat-layout entry: writes a single DLL named /// (already in the /// {stem}.{packageVersion}.dll form), a sibling .meta, /// and updates the manifest to associate it with /// at . /// void CreateFlatPackage(string id, string version, string dllName) { var dllPath = Path.Combine(_installPath, dllName); File.WriteAllText(dllPath, "dummy"); File.WriteAllText(dllPath + ".meta", "fileFormatVersion: 2\nguid: 00000000000000000000000000000000\n"); var manifest = NuGetInstallManifest.Load(_installPath); var entry = new InstalledPackage(version); entry.Dlls.Add(dllName); manifest.Packages[id] = entry; NuGetInstallManifest.Save(_installPath, manifest); } } }