@atelierai/uco 1.0.7 → 1.0.8

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 (87) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +1 -1
  3. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +1034 -1034
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +20 -20
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  42. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  43. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  44. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  45. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  46. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  47. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  48. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  49. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  50. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  51. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  52. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  53. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  54. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  55. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  56. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  57. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  58. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  59. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  60. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
  61. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  62. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  63. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  64. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  65. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  66. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  67. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  68. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  69. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  86. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  87. /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
+ "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, "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
+ }
@@ -15,7 +15,7 @@ using System.Text.Json;
15
15
  using System.Text.Json.Serialization;
16
16
  using com.AtelierAI.Unity.Copilot.Runtime.Utils;
17
17
  using NUnit.Framework;
18
- using static com.AtelierAI.Uco.Framework.Common.Consts.MCP.Server;
18
+ using static com.AtelierAI.Uco.Framework.Common.Consts.Uco.Server;
19
19
 
20
20
  namespace com.AtelierAI.Unity.Copilot.Editor.Tests
21
21
  {