@atelierai/uco 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +29 -8
  2. package/package.json +2 -2
  3. package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +887 -1034
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +22 -22
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  42. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  43. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  44. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  45. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
  46. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
  47. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
  48. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
  49. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
  50. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
  51. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
  52. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
  53. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
  54. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
  55. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  56. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  57. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  58. package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
  59. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  60. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  61. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  62. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  63. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  64. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  65. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  66. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  67. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  68. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  69. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +2 -2
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  86. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  87. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  88. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  89. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  90. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  91. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  92. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  93. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
  94. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  95. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  96. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  97. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  98. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  99. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  100. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  101. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  102. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  103. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  104. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  105. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  106. /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
  107. /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
@@ -1,304 +1,304 @@
1
- /*
2
- +------------------------------------------------------------------+
3
- | Author: Ivan Murzak (https://github.com/IvanMurzak) |
4
- | Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) |
5
- | Copyright (c) 2025 Ivan Murzak |
6
- | Licensed under the Apache License, Version 2.0. |
7
- | See the LICENSE file in the project root for more information. |
8
- +------------------------------------------------------------------+
9
- */
10
-
11
- #nullable enable
12
- using System.IO;
13
- using System.Runtime.InteropServices;
14
- using System.Text.RegularExpressions;
15
- using NUnit.Framework;
16
- using UnityEngine.TestTools;
17
- using com.AtelierAI.Unity.Copilot.Editor.DependencyResolver;
18
-
19
- namespace com.AtelierAI.Unity.Copilot.Editor.Tests.DependencyResolverTests
20
- {
21
- /// <summary>
22
- /// Coverage for issue #733's mandatory legacy → flat-layout migration. Every
23
- /// existing user upgrading to the new resolver still has DLLs sitting under
24
- /// <c>Assets/Plugins/NuGet/{Id}.{Version}/</c> from a pre-fix install. The
25
- /// migration MUST delete those folders before any flat-layout DLL is
26
- /// written, in the same restore cycle, otherwise the project ends up with
27
- /// duplicate copies of every assembly and Unity's compiler errors with
28
- /// CS0436 / CS0433.
29
- /// </summary>
30
- [TestFixture]
31
- public class NuGetLegacyMigrationTests
32
- {
33
- string _installPath = string.Empty;
34
-
35
- [SetUp]
36
- public void SetUp()
37
- {
38
- _installPath = Path.Combine(
39
- Path.GetTempPath(),
40
- "UnityMcp-Migration-" + Path.GetRandomFileName());
41
- Directory.CreateDirectory(_installPath);
42
- }
43
-
44
- [TearDown]
45
- public void TearDown()
46
- {
47
- if (Directory.Exists(_installPath))
48
- {
49
- try { Directory.Delete(_installPath, recursive: true); }
50
- catch { /* best-effort cleanup */ }
51
- }
52
- }
53
-
54
- [Test]
55
- public void Run_NoLegacyState_ReturnsNoLegacyState_OnEmptyInstallPath()
56
- {
57
- var result = NuGetLegacyMigration.Run(_installPath);
58
-
59
- Assert.AreEqual(NuGetLegacyMigration.Outcome.NoLegacyState, result.Outcome);
60
- Assert.AreEqual(0, result.RemovedItems.Count);
61
- }
62
-
63
- [Test]
64
- public void Run_NoLegacyState_ReturnsNoLegacyState_OnUnversionedFlatInstall()
65
- {
66
- // Steady-state install in the unversioned flat layout: no legacy
67
- // {Id}.{Version}/ directories, no versioned-filename {stem}.{v}.dll
68
- // siblings — the migration must short-circuit and touch nothing.
69
- File.WriteAllText(Path.Combine(_installPath, "System.Memory.dll"), "dummy");
70
- File.WriteAllText(Path.Combine(_installPath, "System.Memory.dll.meta"), "meta");
71
- File.WriteAllText(Path.Combine(_installPath, ".nuget-installed.json"), "{}");
72
-
73
- var result = NuGetLegacyMigration.Run(_installPath);
74
-
75
- Assert.AreEqual(NuGetLegacyMigration.Outcome.NoLegacyState, result.Outcome);
76
- Assert.IsTrue(File.Exists(Path.Combine(_installPath, "System.Memory.dll")));
77
- }
78
-
79
- [Test]
80
- public void Run_HappyPath_RemovesLegacyDirectoriesAndMetas()
81
- {
82
- // Seed several legacy directories: single-DLL package and a multi-DLL package.
83
- CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
84
- CreateLegacyPackage("Microsoft.Bcl.Memory", "10.0.3",
85
- "System.Memory.dll", "System.Buffers.dll", "System.Runtime.CompilerServices.Unsafe.dll");
86
-
87
- var result = NuGetLegacyMigration.Run(_installPath);
88
-
89
- Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
90
- Assert.AreEqual(2, result.RemovedItems.Count);
91
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
92
- Assert.IsFalse(File.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5.meta")));
93
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3")));
94
- Assert.IsFalse(File.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3.meta")));
95
- }
96
-
97
- [Test]
98
- public void Run_Idempotent_SecondRunIsNoOp()
99
- {
100
- CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
101
-
102
- var first = NuGetLegacyMigration.Run(_installPath);
103
- Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, first.Outcome);
104
-
105
- var second = NuGetLegacyMigration.Run(_installPath);
106
- Assert.AreEqual(NuGetLegacyMigration.Outcome.NoLegacyState, second.Outcome);
107
- Assert.AreEqual(0, second.RemovedItems.Count);
108
- }
109
-
110
- [Test]
111
- public void Run_MixedLegacyState_MigratesLegacyDirsAndVersionedFiles_LeavesUnrelatedContent()
112
- {
113
- // Mixed install: a legacy {Id}.{Version}/ folder, a flat
114
- // versioned-filename DLL, and unrelated user content. The migration
115
- // sweeps both legacy artifacts in one pass and leaves the user's
116
- // directory + unversioned canonical untouched.
117
- CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
118
- File.WriteAllText(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3.dll"), "flat-versioned-leftover");
119
- File.WriteAllText(Path.Combine(_installPath, "Microsoft.Bcl.Memory.dll"), "canonical");
120
- Directory.CreateDirectory(Path.Combine(_installPath, "ReadMe"));
121
- File.WriteAllText(Path.Combine(_installPath, "ReadMe", "notes.txt"), "user notes");
122
-
123
- var result = NuGetLegacyMigration.Run(_installPath);
124
-
125
- Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
126
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
127
- Assert.IsFalse(File.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3.dll")),
128
- "Versioned-filename flat DLLs are now legacy and must be swept.");
129
- Assert.IsTrue(File.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.dll")),
130
- "Unversioned canonical filenames must not be touched by migration.");
131
- Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "ReadMe")),
132
- "Non-package directories must not be touched by migration.");
133
- }
134
-
135
- [Test]
136
- public void Run_VersionedFilenameDllsAtRoot_AreSwept()
137
- {
138
- // Migration from the pre-unversioned-filename resolver: any flat
139
- // {stem}.{numericVersion}.dll at the install root is by
140
- // definition stale once the canonical filename is just {stem}.dll.
141
- // The migration sweeps them in the same pass that removes
142
- // legacy {Id}.{Version}/ directories.
143
- File.WriteAllText(Path.Combine(_installPath, "McpPlugin.6.2.1.dll"), "stale");
144
- File.WriteAllText(Path.Combine(_installPath, "McpPlugin.6.2.1.dll.meta"), "meta");
145
- File.WriteAllText(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll"), "stale");
146
- File.WriteAllText(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll.meta"), "meta");
147
- // Unversioned canonicals must NOT be touched.
148
- File.WriteAllText(Path.Combine(_installPath, "ReadMe.dll"), "user-content");
149
- File.WriteAllText(Path.Combine(_installPath, "Foo.dll"), "canonical");
150
-
151
- var result = NuGetLegacyMigration.Run(_installPath);
152
-
153
- Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
154
- Assert.IsFalse(File.Exists(Path.Combine(_installPath, "McpPlugin.6.2.1.dll")));
155
- Assert.IsFalse(File.Exists(Path.Combine(_installPath, "McpPlugin.6.2.1.dll.meta")),
156
- ".meta sidecar must be deleted alongside the DLL.");
157
- Assert.IsFalse(File.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll")));
158
- Assert.IsTrue(File.Exists(Path.Combine(_installPath, "ReadMe.dll")),
159
- "Unversioned filenames are the new canonical and must be preserved.");
160
- Assert.IsTrue(File.Exists(Path.Combine(_installPath, "Foo.dll")));
161
- }
162
-
163
- [Test]
164
- public void Run_LegacyDirWithSemVerPrerelease_RemovedAsLegacy()
165
- {
166
- // NuGet folder names can contain SemVer prerelease / build-metadata
167
- // suffixes (e.g. `Microsoft.Extensions.Logging.8.0.1-preview`).
168
- // System.Version.TryParse rejects those tails, so an earlier version
169
- // of the migration silently left those folders on disk and the user
170
- // ended up with both the legacy nested DLL and the new flat-layout
171
- // DLL coexisting — duplicate-assembly compile errors. Pin the
172
- // SemVer-shape fallback in `ExtractPackageIdFromDirName` so the
173
- // migration removes them.
174
- CreateLegacyPackage("Foo.Bar", "1.0.0-preview", "Foo.Bar.dll");
175
- CreateLegacyPackage("Baz", "2.3.4+build.42", "Baz.dll");
176
- CreateLegacyPackage("Qux", "1.0.0-rc.1", "Qux.dll");
177
-
178
- var result = NuGetLegacyMigration.Run(_installPath);
179
-
180
- Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
181
- Assert.AreEqual(3, result.RemovedItems.Count);
182
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Foo.Bar.1.0.0-preview")));
183
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Baz.2.3.4+build.42")));
184
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Qux.1.0.0-rc.1")));
185
- }
186
-
187
- #if UNITY_EDITOR_WIN
188
- // Windows-only: simulates a `FileShare.None` lock that Unity holds on a
189
- // DLL it has loaded into the editor AppDomain. Linux/macOS use advisory
190
- // locking — `FileShare.None` does not prevent `File.Delete` there — so
191
- // gating on `UNITY_EDITOR_WIN` keeps the assertion honest. The
192
- // post-best-effort migration no longer aborts the entire restore on a
193
- // single locked file: the call returns `AbortedFileLock` to surface
194
- // that some directory is still on disk, but the caller may continue
195
- // safely (the locked file's PluginImporter is disabled so the next
196
- // domain reload unloads it and the next migration pass cleans it up).
197
- [Test]
198
- public void Run_FileLock_LeavesBlockedFolderIntactAndReportsAbortedFileLock()
199
- {
200
- CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
201
- var lockedDll = Path.Combine(_installPath, "System.Text.Json.8.0.5", "System.Text.Json.dll");
202
-
203
- // Best-effort migration logs a warning (not an error) for blocked folders.
204
- LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(@"\[NuGet\] Could not fully remove legacy install directory"));
205
-
206
- using (var lockHandle = new FileStream(lockedDll, FileMode.Open, FileAccess.Read, FileShare.None))
207
- {
208
- var result = NuGetLegacyMigration.Run(_installPath);
209
-
210
- Assert.AreEqual(NuGetLegacyMigration.Outcome.AbortedFileLock, result.Outcome);
211
- Assert.IsNotNull(result.FirstFailedItem);
212
- Assert.IsNotNull(result.FirstFailureMessage);
213
- // Blocked folder still on disk — the next reload picks it back up.
214
- Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
215
- }
216
- }
217
-
218
- [Test]
219
- public void Run_FileLock_OneFolderBlocked_OtherFoldersAreStillRemoved()
220
- {
221
- // Mixed state: a blocked folder must NOT prevent unblocked folders
222
- // from being cleaned up — that's the whole point of best-effort.
223
- CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
224
- CreateLegacyPackage("R3", "1.3.0", "R3.dll");
225
- var lockedDll = Path.Combine(_installPath, "System.Text.Json.8.0.5", "System.Text.Json.dll");
226
-
227
- LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(@"\[NuGet\] Could not fully remove legacy install directory"));
228
-
229
- using (var lockHandle = new FileStream(lockedDll, FileMode.Open, FileAccess.Read, FileShare.None))
230
- {
231
- var result = NuGetLegacyMigration.Run(_installPath);
232
-
233
- Assert.AreEqual(NuGetLegacyMigration.Outcome.AbortedFileLock, result.Outcome);
234
- Assert.AreEqual(1, result.RemovedItems.Count,
235
- "The unblocked folder must still be removed even though a sibling folder was locked.");
236
- Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")),
237
- "Blocked folder remains on disk for retry on the next pass.");
238
- Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "R3.1.3.0")),
239
- "Unblocked folder must have been removed.");
240
- }
241
- }
242
- #endif
243
-
244
- #if !UNITY_EDITOR_WIN
245
- // Unix counterpart of Run_FileLock_LeavesBlockedFolderIntactAndReportsAbortedFileLock.
246
- // The migration catches both `IOException` (Windows file-lock) and
247
- // `UnauthorizedAccessException` (Unix permission denial / antivirus /
248
- // read-only flags) inside its per-file delete loop. Here we deny write
249
- // on the install-path parent via `chmod 0o500`, which makes
250
- // `File.Delete` and the trailing `Directory.Delete` both throw
251
- // `UnauthorizedAccessException`. Self-skips when running as root.
252
- [Test]
253
- public void Run_PermissionDenied_LeavesBlockedFolderIntactAndReportsAbortedFileLock()
254
- {
255
- if (geteuid() == 0)
256
- Assert.Ignore("chmod-based permission denial is bypassed by the kernel for uid=0; run this test as a non-root user.");
257
-
258
- CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
259
-
260
- LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(@"\[NuGet\] Could not fully remove legacy install directory"));
261
-
262
- const uint READ_EXEC_ONLY = 0b101_000_000; // 0o500 — r-x------
263
- const uint READ_WRITE_EXEC = 0b111_000_000; // 0o700 — rwx------ (TearDown-friendly)
264
- if (chmod(_installPath, READ_EXEC_ONLY) != 0)
265
- Assert.Inconclusive($"chmod {_installPath} -> 0o500 failed (errno={Marshal.GetLastWin32Error()})");
266
-
267
- try
268
- {
269
- var result = NuGetLegacyMigration.Run(_installPath);
270
-
271
- Assert.AreEqual(NuGetLegacyMigration.Outcome.AbortedFileLock, result.Outcome);
272
- Assert.IsNotNull(result.FirstFailedItem);
273
- Assert.IsNotNull(result.FirstFailureMessage);
274
- Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
275
- }
276
- finally
277
- {
278
- // Always restore so [TearDown]'s recursive delete of `_installPath` succeeds.
279
- chmod(_installPath, READ_WRITE_EXEC);
280
- }
281
- }
282
-
283
- [DllImport("libc", SetLastError = true)]
284
- static extern int chmod(string pathname, uint mode);
285
-
286
- [DllImport("libc", SetLastError = true)]
287
- static extern uint geteuid();
288
- #endif
289
-
290
- /// <summary>
291
- /// Creates a fake legacy <c>{Id}.{Version}/</c> directory with the
292
- /// given DLL filenames inside, plus the sibling <c>.meta</c>.
293
- /// </summary>
294
- void CreateLegacyPackage(string id, string version, params string[] dllNames)
295
- {
296
- var dirName = $"{id}.{version}";
297
- var dirPath = Path.Combine(_installPath, dirName);
298
- Directory.CreateDirectory(dirPath);
299
- foreach (var dll in dllNames)
300
- File.WriteAllText(Path.Combine(dirPath, dll), "dummy");
301
- File.WriteAllText(dirPath + ".meta", "fileFormatVersion: 2\nguid: 00000000000000000000000000000000\n");
302
- }
303
- }
304
- }
1
+ /*
2
+ +------------------------------------------------------------------+
3
+ | Author: Ivan Murzak (https://github.com/IvanMurzak) |
4
+ | Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) |
5
+ | Copyright (c) 2025 Ivan Murzak |
6
+ | Licensed under the Apache License, Version 2.0. |
7
+ | See the LICENSE file in the project root for more information. |
8
+ +------------------------------------------------------------------+
9
+ */
10
+
11
+ #nullable enable
12
+ using System.IO;
13
+ using System.Runtime.InteropServices;
14
+ using System.Text.RegularExpressions;
15
+ using NUnit.Framework;
16
+ using UnityEngine.TestTools;
17
+ using com.AtelierAI.Unity.Copilot.Editor.DependencyResolver;
18
+
19
+ namespace com.AtelierAI.Unity.Copilot.Editor.Tests.DependencyResolverTests
20
+ {
21
+ /// <summary>
22
+ /// Coverage for issue #733's mandatory legacy → flat-layout migration. Every
23
+ /// existing user upgrading to the new resolver still has DLLs sitting under
24
+ /// <c>Assets/Plugins/NuGet/{Id}.{Version}/</c> from a pre-fix install. The
25
+ /// migration MUST delete those folders before any flat-layout DLL is
26
+ /// written, in the same restore cycle, otherwise the project ends up with
27
+ /// duplicate copies of every assembly and Unity's compiler errors with
28
+ /// CS0436 / CS0433.
29
+ /// </summary>
30
+ [TestFixture]
31
+ public class NuGetLegacyMigrationTests
32
+ {
33
+ string _installPath = string.Empty;
34
+
35
+ [SetUp]
36
+ public void SetUp()
37
+ {
38
+ _installPath = Path.Combine(
39
+ Path.GetTempPath(),
40
+ "Uco-Migration-" + Path.GetRandomFileName());
41
+ Directory.CreateDirectory(_installPath);
42
+ }
43
+
44
+ [TearDown]
45
+ public void TearDown()
46
+ {
47
+ if (Directory.Exists(_installPath))
48
+ {
49
+ try { Directory.Delete(_installPath, recursive: true); }
50
+ catch { /* best-effort cleanup */ }
51
+ }
52
+ }
53
+
54
+ [Test]
55
+ public void Run_NoLegacyState_ReturnsNoLegacyState_OnEmptyInstallPath()
56
+ {
57
+ var result = NuGetLegacyMigration.Run(_installPath);
58
+
59
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.NoLegacyState, result.Outcome);
60
+ Assert.AreEqual(0, result.RemovedItems.Count);
61
+ }
62
+
63
+ [Test]
64
+ public void Run_NoLegacyState_ReturnsNoLegacyState_OnUnversionedFlatInstall()
65
+ {
66
+ // Steady-state install in the unversioned flat layout: no legacy
67
+ // {Id}.{Version}/ directories, no versioned-filename {stem}.{v}.dll
68
+ // siblings — the migration must short-circuit and touch nothing.
69
+ File.WriteAllText(Path.Combine(_installPath, "System.Memory.dll"), "dummy");
70
+ File.WriteAllText(Path.Combine(_installPath, "System.Memory.dll.meta"), "meta");
71
+ File.WriteAllText(Path.Combine(_installPath, ".nuget-installed.json"), "{}");
72
+
73
+ var result = NuGetLegacyMigration.Run(_installPath);
74
+
75
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.NoLegacyState, result.Outcome);
76
+ Assert.IsTrue(File.Exists(Path.Combine(_installPath, "System.Memory.dll")));
77
+ }
78
+
79
+ [Test]
80
+ public void Run_HappyPath_RemovesLegacyDirectoriesAndMetas()
81
+ {
82
+ // Seed several legacy directories: single-DLL package and a multi-DLL package.
83
+ CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
84
+ CreateLegacyPackage("Microsoft.Bcl.Memory", "10.0.3",
85
+ "System.Memory.dll", "System.Buffers.dll", "System.Runtime.CompilerServices.Unsafe.dll");
86
+
87
+ var result = NuGetLegacyMigration.Run(_installPath);
88
+
89
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
90
+ Assert.AreEqual(2, result.RemovedItems.Count);
91
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
92
+ Assert.IsFalse(File.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5.meta")));
93
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3")));
94
+ Assert.IsFalse(File.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3.meta")));
95
+ }
96
+
97
+ [Test]
98
+ public void Run_Idempotent_SecondRunIsNoOp()
99
+ {
100
+ CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
101
+
102
+ var first = NuGetLegacyMigration.Run(_installPath);
103
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, first.Outcome);
104
+
105
+ var second = NuGetLegacyMigration.Run(_installPath);
106
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.NoLegacyState, second.Outcome);
107
+ Assert.AreEqual(0, second.RemovedItems.Count);
108
+ }
109
+
110
+ [Test]
111
+ public void Run_MixedLegacyState_MigratesLegacyDirsAndVersionedFiles_LeavesUnrelatedContent()
112
+ {
113
+ // Mixed install: a legacy {Id}.{Version}/ folder, a flat
114
+ // versioned-filename DLL, and unrelated user content. The migration
115
+ // sweeps both legacy artifacts in one pass and leaves the user's
116
+ // directory + unversioned canonical untouched.
117
+ CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
118
+ File.WriteAllText(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3.dll"), "flat-versioned-leftover");
119
+ File.WriteAllText(Path.Combine(_installPath, "Microsoft.Bcl.Memory.dll"), "canonical");
120
+ Directory.CreateDirectory(Path.Combine(_installPath, "ReadMe"));
121
+ File.WriteAllText(Path.Combine(_installPath, "ReadMe", "notes.txt"), "user notes");
122
+
123
+ var result = NuGetLegacyMigration.Run(_installPath);
124
+
125
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
126
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
127
+ Assert.IsFalse(File.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.10.0.3.dll")),
128
+ "Versioned-filename flat DLLs are now legacy and must be swept.");
129
+ Assert.IsTrue(File.Exists(Path.Combine(_installPath, "Microsoft.Bcl.Memory.dll")),
130
+ "Unversioned canonical filenames must not be touched by migration.");
131
+ Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "ReadMe")),
132
+ "Non-package directories must not be touched by migration.");
133
+ }
134
+
135
+ [Test]
136
+ public void Run_VersionedFilenameDllsAtRoot_AreSwept()
137
+ {
138
+ // Migration from the pre-unversioned-filename resolver: any flat
139
+ // {stem}.{numericVersion}.dll at the install root is by
140
+ // definition stale once the canonical filename is just {stem}.dll.
141
+ // The migration sweeps them in the same pass that removes
142
+ // legacy {Id}.{Version}/ directories.
143
+ File.WriteAllText(Path.Combine(_installPath, "UcoPlugin.6.2.1.dll"), "stale");
144
+ File.WriteAllText(Path.Combine(_installPath, "UcoPlugin.6.2.1.dll.meta"), "meta");
145
+ File.WriteAllText(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll"), "stale");
146
+ File.WriteAllText(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll.meta"), "meta");
147
+ // Unversioned canonicals must NOT be touched.
148
+ File.WriteAllText(Path.Combine(_installPath, "ReadMe.dll"), "user-content");
149
+ File.WriteAllText(Path.Combine(_installPath, "Foo.dll"), "canonical");
150
+
151
+ var result = NuGetLegacyMigration.Run(_installPath);
152
+
153
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
154
+ Assert.IsFalse(File.Exists(Path.Combine(_installPath, "UcoPlugin.6.2.1.dll")));
155
+ Assert.IsFalse(File.Exists(Path.Combine(_installPath, "UcoPlugin.6.2.1.dll.meta")),
156
+ ".meta sidecar must be deleted alongside the DLL.");
157
+ Assert.IsFalse(File.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll")));
158
+ Assert.IsTrue(File.Exists(Path.Combine(_installPath, "ReadMe.dll")),
159
+ "Unversioned filenames are the new canonical and must be preserved.");
160
+ Assert.IsTrue(File.Exists(Path.Combine(_installPath, "Foo.dll")));
161
+ }
162
+
163
+ [Test]
164
+ public void Run_LegacyDirWithSemVerPrerelease_RemovedAsLegacy()
165
+ {
166
+ // NuGet folder names can contain SemVer prerelease / build-metadata
167
+ // suffixes (e.g. `Microsoft.Extensions.Logging.8.0.1-preview`).
168
+ // System.Version.TryParse rejects those tails, so an earlier version
169
+ // of the migration silently left those folders on disk and the user
170
+ // ended up with both the legacy nested DLL and the new flat-layout
171
+ // DLL coexisting — duplicate-assembly compile errors. Pin the
172
+ // SemVer-shape fallback in `ExtractPackageIdFromDirName` so the
173
+ // migration removes them.
174
+ CreateLegacyPackage("Foo.Bar", "1.0.0-preview", "Foo.Bar.dll");
175
+ CreateLegacyPackage("Baz", "2.3.4+build.42", "Baz.dll");
176
+ CreateLegacyPackage("Qux", "1.0.0-rc.1", "Qux.dll");
177
+
178
+ var result = NuGetLegacyMigration.Run(_installPath);
179
+
180
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.Migrated, result.Outcome);
181
+ Assert.AreEqual(3, result.RemovedItems.Count);
182
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Foo.Bar.1.0.0-preview")));
183
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Baz.2.3.4+build.42")));
184
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "Qux.1.0.0-rc.1")));
185
+ }
186
+
187
+ #if UNITY_EDITOR_WIN
188
+ // Windows-only: simulates a `FileShare.None` lock that Unity holds on a
189
+ // DLL it has loaded into the editor AppDomain. Linux/macOS use advisory
190
+ // locking — `FileShare.None` does not prevent `File.Delete` there — so
191
+ // gating on `UNITY_EDITOR_WIN` keeps the assertion honest. The
192
+ // post-best-effort migration no longer aborts the entire restore on a
193
+ // single locked file: the call returns `AbortedFileLock` to surface
194
+ // that some directory is still on disk, but the caller may continue
195
+ // safely (the locked file's PluginImporter is disabled so the next
196
+ // domain reload unloads it and the next migration pass cleans it up).
197
+ [Test]
198
+ public void Run_FileLock_LeavesBlockedFolderIntactAndReportsAbortedFileLock()
199
+ {
200
+ CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
201
+ var lockedDll = Path.Combine(_installPath, "System.Text.Json.8.0.5", "System.Text.Json.dll");
202
+
203
+ // Best-effort migration logs a warning (not an error) for blocked folders.
204
+ LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(@"\[NuGet\] Could not fully remove legacy install directory"));
205
+
206
+ using (var lockHandle = new FileStream(lockedDll, FileMode.Open, FileAccess.Read, FileShare.None))
207
+ {
208
+ var result = NuGetLegacyMigration.Run(_installPath);
209
+
210
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.AbortedFileLock, result.Outcome);
211
+ Assert.IsNotNull(result.FirstFailedItem);
212
+ Assert.IsNotNull(result.FirstFailureMessage);
213
+ // Blocked folder still on disk — the next reload picks it back up.
214
+ Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
215
+ }
216
+ }
217
+
218
+ [Test]
219
+ public void Run_FileLock_OneFolderBlocked_OtherFoldersAreStillRemoved()
220
+ {
221
+ // Mixed state: a blocked folder must NOT prevent unblocked folders
222
+ // from being cleaned up — that's the whole point of best-effort.
223
+ CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
224
+ CreateLegacyPackage("R3", "1.3.0", "R3.dll");
225
+ var lockedDll = Path.Combine(_installPath, "System.Text.Json.8.0.5", "System.Text.Json.dll");
226
+
227
+ LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(@"\[NuGet\] Could not fully remove legacy install directory"));
228
+
229
+ using (var lockHandle = new FileStream(lockedDll, FileMode.Open, FileAccess.Read, FileShare.None))
230
+ {
231
+ var result = NuGetLegacyMigration.Run(_installPath);
232
+
233
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.AbortedFileLock, result.Outcome);
234
+ Assert.AreEqual(1, result.RemovedItems.Count,
235
+ "The unblocked folder must still be removed even though a sibling folder was locked.");
236
+ Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")),
237
+ "Blocked folder remains on disk for retry on the next pass.");
238
+ Assert.IsFalse(Directory.Exists(Path.Combine(_installPath, "R3.1.3.0")),
239
+ "Unblocked folder must have been removed.");
240
+ }
241
+ }
242
+ #endif
243
+
244
+ #if !UNITY_EDITOR_WIN
245
+ // Unix counterpart of Run_FileLock_LeavesBlockedFolderIntactAndReportsAbortedFileLock.
246
+ // The migration catches both `IOException` (Windows file-lock) and
247
+ // `UnauthorizedAccessException` (Unix permission denial / antivirus /
248
+ // read-only flags) inside its per-file delete loop. Here we deny write
249
+ // on the install-path parent via `chmod 0o500`, which makes
250
+ // `File.Delete` and the trailing `Directory.Delete` both throw
251
+ // `UnauthorizedAccessException`. Self-skips when running as root.
252
+ [Test]
253
+ public void Run_PermissionDenied_LeavesBlockedFolderIntactAndReportsAbortedFileLock()
254
+ {
255
+ if (geteuid() == 0)
256
+ Assert.Ignore("chmod-based permission denial is bypassed by the kernel for uid=0; run this test as a non-root user.");
257
+
258
+ CreateLegacyPackage("System.Text.Json", "8.0.5", "System.Text.Json.dll");
259
+
260
+ LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(@"\[NuGet\] Could not fully remove legacy install directory"));
261
+
262
+ const uint READ_EXEC_ONLY = 0b101_000_000; // 0o500 — r-x------
263
+ const uint READ_WRITE_EXEC = 0b111_000_000; // 0o700 — rwx------ (TearDown-friendly)
264
+ if (chmod(_installPath, READ_EXEC_ONLY) != 0)
265
+ Assert.Inconclusive($"chmod {_installPath} -> 0o500 failed (errno={Marshal.GetLastWin32Error()})");
266
+
267
+ try
268
+ {
269
+ var result = NuGetLegacyMigration.Run(_installPath);
270
+
271
+ Assert.AreEqual(NuGetLegacyMigration.Outcome.AbortedFileLock, result.Outcome);
272
+ Assert.IsNotNull(result.FirstFailedItem);
273
+ Assert.IsNotNull(result.FirstFailureMessage);
274
+ Assert.IsTrue(Directory.Exists(Path.Combine(_installPath, "System.Text.Json.8.0.5")));
275
+ }
276
+ finally
277
+ {
278
+ // Always restore so [TearDown]'s recursive delete of `_installPath` succeeds.
279
+ chmod(_installPath, READ_WRITE_EXEC);
280
+ }
281
+ }
282
+
283
+ [DllImport("libc", SetLastError = true)]
284
+ static extern int chmod(string pathname, uint mode);
285
+
286
+ [DllImport("libc", SetLastError = true)]
287
+ static extern uint geteuid();
288
+ #endif
289
+
290
+ /// <summary>
291
+ /// Creates a fake legacy <c>{Id}.{Version}/</c> directory with the
292
+ /// given DLL filenames inside, plus the sibling <c>.meta</c>.
293
+ /// </summary>
294
+ void CreateLegacyPackage(string id, string version, params string[] dllNames)
295
+ {
296
+ var dirName = $"{id}.{version}";
297
+ var dirPath = Path.Combine(_installPath, dirName);
298
+ Directory.CreateDirectory(dirPath);
299
+ foreach (var dll in dllNames)
300
+ File.WriteAllText(Path.Combine(dirPath, dll), "dummy");
301
+ File.WriteAllText(dirPath + ".meta", "fileFormatVersion: 2\nguid: 00000000000000000000000000000000\n");
302
+ }
303
+ }
304
+ }