@atelierai/uco 1.0.8 → 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 (31) hide show
  1. package/CHANGELOG.md +18 -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/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +0 -147
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +3 -3
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
  18. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  19. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  20. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  21. package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
  22. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +1 -1
  23. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
  24. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
  25. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
  26. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +1 -1
  27. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
  28. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
  29. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
  30. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  31. /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
@@ -1,436 +1,436 @@
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.Linq;
14
- using NUnit.Framework;
15
- using com.AtelierAI.Unity.Copilot.Editor.DependencyResolver;
16
-
17
- namespace com.AtelierAI.Unity.Copilot.Editor.Tests.DependencyResolverTests
18
- {
19
- /// <summary>
20
- /// Coverage for the on-disk manifest <c>.nuget-installed.json</c> introduced
21
- /// for the flat layout (issue #733). The manifest is the primary source of
22
- /// truth for "which DLL belongs to which package at which version", with
23
- /// versioned-filename parsing as the disaster-recovery fallback.
24
- /// </summary>
25
- [TestFixture]
26
- public class NuGetInstallManifestTests
27
- {
28
- string _installPath = string.Empty;
29
-
30
- [SetUp]
31
- public void SetUp()
32
- {
33
- _installPath = Path.Combine(
34
- Path.GetTempPath(),
35
- "UnityMcp-Manifest-" + Path.GetRandomFileName());
36
- Directory.CreateDirectory(_installPath);
37
- }
38
-
39
- [TearDown]
40
- public void TearDown()
41
- {
42
- if (Directory.Exists(_installPath))
43
- {
44
- try { Directory.Delete(_installPath, recursive: true); }
45
- catch { /* best-effort cleanup */ }
46
- }
47
- }
48
-
49
- [Test]
50
- public void Load_ReturnsEmptyManifest_WhenFileMissing()
51
- {
52
- var manifest = NuGetInstallManifest.Load(_installPath);
53
- Assert.AreEqual(0, manifest.Packages.Count);
54
- }
55
-
56
- [Test]
57
- public void SaveThenLoad_RoundTrips_AllFields()
58
- {
59
- var manifest = new InstallManifest();
60
- var entry = new InstalledPackage("8.0.1");
61
- entry.Dlls.Add("Microsoft.Extensions.Logging.8.0.1.dll");
62
- entry.Dlls.Add("Microsoft.Extensions.Logging.Abstractions.8.0.3.dll");
63
- manifest.Packages["Microsoft.Extensions.Logging"] = entry;
64
-
65
- // Multi-DLL package with a different version.
66
- var multi = new InstalledPackage("10.0.3");
67
- multi.Dlls.Add("System.Memory.10.0.3.dll");
68
- multi.Dlls.Add("System.Buffers.10.0.3.dll");
69
- manifest.Packages["Microsoft.Bcl.Memory"] = multi;
70
-
71
- // Empty-DLL entry (development-only dependency).
72
- manifest.Packages["Microsoft.CodeAnalysis.Analyzers"] = new InstalledPackage("3.11.0");
73
-
74
- NuGetInstallManifest.Save(_installPath, manifest);
75
-
76
- var roundTrip = NuGetInstallManifest.Load(_installPath);
77
-
78
- Assert.AreEqual(3, roundTrip.Packages.Count);
79
- Assert.AreEqual("8.0.1", roundTrip.Packages["Microsoft.Extensions.Logging"].Version);
80
- Assert.AreEqual(2, roundTrip.Packages["Microsoft.Extensions.Logging"].Dlls.Count);
81
- Assert.AreEqual("10.0.3", roundTrip.Packages["Microsoft.Bcl.Memory"].Version);
82
- CollectionAssert.AreEquivalent(
83
- new[] { "System.Memory.10.0.3.dll", "System.Buffers.10.0.3.dll" },
84
- roundTrip.Packages["Microsoft.Bcl.Memory"].Dlls);
85
- Assert.AreEqual("3.11.0", roundTrip.Packages["Microsoft.CodeAnalysis.Analyzers"].Version);
86
- Assert.AreEqual(0, roundTrip.Packages["Microsoft.CodeAnalysis.Analyzers"].Dlls.Count);
87
- }
88
-
89
- [Test]
90
- public void Save_CreatesInstallDirectoryIfMissing()
91
- {
92
- var nested = Path.Combine(_installPath, "nested-not-yet-created");
93
- Assert.IsFalse(Directory.Exists(nested));
94
-
95
- var manifest = new InstallManifest();
96
- NuGetInstallManifest.Save(nested, manifest);
97
-
98
- Assert.IsTrue(Directory.Exists(nested));
99
- Assert.IsTrue(File.Exists(Path.Combine(nested, ".nuget-installed.json")));
100
- }
101
-
102
- [Test]
103
- public void Load_ReturnsEmptyManifest_OnMalformedJson_AndDoesNotThrow()
104
- {
105
- File.WriteAllText(Path.Combine(_installPath, ".nuget-installed.json"), "{ not valid json");
106
-
107
- var manifest = NuGetInstallManifest.Load(_installPath);
108
-
109
- Assert.AreEqual(0, manifest.Packages.Count);
110
- }
111
-
112
- [Test]
113
- public void Load_PreservesCaseInsensitiveLookup()
114
- {
115
- // The runtime resolver matches package IDs case-insensitively
116
- // throughout. The manifest must round-trip with the same property.
117
- var manifest = new InstallManifest();
118
- manifest.Packages["System.Text.Json"] = new InstalledPackage("8.0.5");
119
- NuGetInstallManifest.Save(_installPath, manifest);
120
-
121
- var loaded = NuGetInstallManifest.Load(_installPath);
122
- Assert.IsTrue(loaded.Packages.ContainsKey("system.text.json"),
123
- "Package ID lookup must be case-insensitive after a round-trip.");
124
- Assert.IsTrue(loaded.Packages.ContainsKey("SYSTEM.TEXT.JSON"));
125
- }
126
-
127
- [Test]
128
- public void Save_IsIdempotent_RoundTripIsByteForByte()
129
- {
130
- // Two saves of the same logical manifest must produce the exact
131
- // same bytes — keeps git diffs clean and avoids spurious
132
- // post-restore changes.
133
- var manifest = new InstallManifest();
134
- var entry = new InstalledPackage("8.0.5");
135
- entry.Dlls.Add("System.Text.Json.8.0.5.dll");
136
- manifest.Packages["System.Text.Json"] = entry;
137
-
138
- NuGetInstallManifest.Save(_installPath, manifest);
139
- var first = File.ReadAllBytes(Path.Combine(_installPath, ".nuget-installed.json"));
140
-
141
- // Round-trip and save again.
142
- var loaded = NuGetInstallManifest.Load(_installPath);
143
- NuGetInstallManifest.Save(_installPath, loaded);
144
- var second = File.ReadAllBytes(Path.Combine(_installPath, ".nuget-installed.json"));
145
-
146
- CollectionAssert.AreEqual(first, second);
147
- }
148
-
149
- [Test]
150
- public void TryRebuildFromDisk_ReproducesSingleDllPackagesFromVersionedFilenames()
151
- {
152
- // Disaster recovery (#733 acceptance criterion): user deletes
153
- // .nuget-installed.json. The next restore must rebuild the manifest
154
- // from on-disk versioned filenames, with no re-extraction needed.
155
- File.WriteAllText(Path.Combine(_installPath, "Microsoft.Extensions.Logging.8.0.1.dll"), "dummy");
156
- File.WriteAllText(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll"), "dummy");
157
- File.WriteAllText(Path.Combine(_installPath, "R3.1.3.0.dll"), "dummy");
158
- // Plus an unrelated file the rebuild must ignore.
159
- File.WriteAllText(Path.Combine(_installPath, "ReadMe.txt"), "user notes");
160
-
161
- var rebuilt = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
162
-
163
- Assert.AreEqual(3, rebuilt.Packages.Count);
164
- Assert.AreEqual("8.0.1", rebuilt.Packages["Microsoft.Extensions.Logging"].Version);
165
- Assert.AreEqual("8.0.5", rebuilt.Packages["System.Text.Json"].Version);
166
- Assert.AreEqual("1.3.0", rebuilt.Packages["R3"].Version);
167
- }
168
-
169
- [Test]
170
- public void TryRebuildFromDisk_IgnoresLegacyUnversionedDllsAndNonDllFiles()
171
- {
172
- // Pre-flat-layout artifacts the user might still have on disk —
173
- // the parser must reject them so the rebuild stays consistent.
174
- File.WriteAllText(Path.Combine(_installPath, "System.Memory.dll"), "legacy unversioned");
175
- File.WriteAllText(Path.Combine(_installPath, "ReadMe.md"), "notes");
176
-
177
- var rebuilt = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
178
-
179
- Assert.AreEqual(0, rebuilt.Packages.Count);
180
- }
181
-
182
- [Test]
183
- public void TryRebuildFromDisk_KeysMultiDllPackagesUnderSyntheticStems()
184
- {
185
- // Microsoft.Bcl.Memory is the canonical multi-DLL package: it ships
186
- // System.Memory, System.Buffers, System.Runtime.CompilerServices.Unsafe.
187
- // The disaster-recovery rebuild has no way to recover the real owner
188
- // from filenames alone, so it MUST key those DLLs under their own
189
- // stems as synthetic IDs. The follow-up MigrateSyntheticOwnerEntries
190
- // call (driven from NuGetPackageInstaller.Install) reconciles the
191
- // synthetic IDs back onto the real package ID — see the
192
- // ReconcilesSyntheticEntries_ForMultiDllPackage test below.
193
- File.WriteAllText(Path.Combine(_installPath, "System.Memory.10.0.3.dll"), "dummy");
194
- File.WriteAllText(Path.Combine(_installPath, "System.Buffers.10.0.3.dll"), "dummy");
195
- File.WriteAllText(Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll"), "dummy");
196
-
197
- var rebuilt = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
198
-
199
- Assert.AreEqual(3, rebuilt.Packages.Count);
200
- Assert.IsTrue(rebuilt.Packages.ContainsKey("System.Memory"));
201
- Assert.IsTrue(rebuilt.Packages.ContainsKey("System.Buffers"));
202
- Assert.IsTrue(rebuilt.Packages.ContainsKey("System.Runtime.CompilerServices.Unsafe"));
203
- Assert.IsFalse(rebuilt.Packages.ContainsKey("Microsoft.Bcl.Memory"),
204
- "Filename-only rebuild cannot recover the real owning package id; the test guards the synthetic-id contract that MigrateSyntheticOwnerEntries depends on.");
205
- }
206
-
207
- [Test]
208
- public void MigrateSyntheticOwnerEntries_ReconcilesMultiDllPackage_AndDoesNotTripCollisionCheck()
209
- {
210
- // End-to-end coverage of the post-rebuild reconciliation flow:
211
- // 1. Seed a multi-DLL package's flat-layout DLLs without a manifest.
212
- // 2. Run TryRebuildFromDisk — expect synthetic stem-keyed entries.
213
- // 3. Run MigrateSyntheticOwnerEntries with the real package id and a
214
- // planned-DLL list that matches the on-disk filenames.
215
- // 4. Assert the synthetic entries are gone and the real package id
216
- // now owns those same DLLs at the same version.
217
- // Without this fix, NuGetPackageInstaller.Install would log
218
- // "Refusing to install Microsoft.Bcl.Memory ..." and stick the user
219
- // on the disaster-recovery path forever.
220
- File.WriteAllText(Path.Combine(_installPath, "System.Memory.10.0.3.dll"), "dummy");
221
- File.WriteAllText(Path.Combine(_installPath, "System.Buffers.10.0.3.dll"), "dummy");
222
- File.WriteAllText(Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll"), "dummy");
223
-
224
- var manifest = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
225
- // Sanity: the rebuild produced synthetic entries (the [high] reproduction state).
226
- Assert.IsTrue(manifest.Packages.ContainsKey("System.Memory"));
227
- Assert.IsFalse(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"));
228
-
229
- var planned = new System.Collections.Generic.List<PlannedDll>
230
- {
231
- new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", Path.Combine(_installPath, "System.Memory.10.0.3.dll")),
232
- new PlannedDll("lib/net8.0/System.Buffers.dll", "System.Buffers.10.0.3.dll", Path.Combine(_installPath, "System.Buffers.10.0.3.dll")),
233
- new PlannedDll("lib/net8.0/System.Runtime.CompilerServices.Unsafe.dll", "System.Runtime.CompilerServices.Unsafe.10.0.3.dll", Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll")),
234
- };
235
-
236
- NuGetPackageInstaller.MigrateSyntheticOwnerEntries(manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
237
-
238
- Assert.IsFalse(manifest.Packages.ContainsKey("System.Memory"),
239
- "Synthetic stem-keyed entry must be removed after reconciliation.");
240
- Assert.IsFalse(manifest.Packages.ContainsKey("System.Buffers"));
241
- Assert.IsFalse(manifest.Packages.ContainsKey("System.Runtime.CompilerServices.Unsafe"));
242
- Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"),
243
- "Real package id must own the migrated DLLs after reconciliation.");
244
- Assert.AreEqual("10.0.3", manifest.Packages["Microsoft.Bcl.Memory"].Version);
245
- CollectionAssert.AreEquivalent(
246
- new[]
247
- {
248
- "System.Memory.10.0.3.dll",
249
- "System.Buffers.10.0.3.dll",
250
- "System.Runtime.CompilerServices.Unsafe.10.0.3.dll",
251
- },
252
- manifest.Packages["Microsoft.Bcl.Memory"].Dlls);
253
- }
254
-
255
- [Test]
256
- public void MigrateSyntheticOwnerEntries_DoesNotTouchUnrelatedPackages()
257
- {
258
- // Defense: the migrator must only strip entries whose DLL set is a
259
- // subset of the planned filenames. A real distinct package that
260
- // happens to share a version number must survive untouched.
261
- var manifest = new InstallManifest();
262
- var unrelatedEntry = new InstalledPackage("10.0.3");
263
- unrelatedEntry.Dlls.Add("Newtonsoft.Json.10.0.3.dll");
264
- manifest.Packages["Newtonsoft.Json"] = unrelatedEntry;
265
-
266
- var syntheticEntry = new InstalledPackage("10.0.3");
267
- syntheticEntry.Dlls.Add("System.Memory.10.0.3.dll");
268
- manifest.Packages["System.Memory"] = syntheticEntry;
269
-
270
- var planned = new System.Collections.Generic.List<PlannedDll>
271
- {
272
- new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", "/ignored/System.Memory.10.0.3.dll"),
273
- };
274
-
275
- NuGetPackageInstaller.MigrateSyntheticOwnerEntries(manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
276
-
277
- Assert.IsTrue(manifest.Packages.ContainsKey("Newtonsoft.Json"),
278
- "Unrelated same-version package must not be migrated away.");
279
- Assert.IsFalse(manifest.Packages.ContainsKey("System.Memory"),
280
- "Synthetic same-version entry whose DLLs match the planned set must be migrated.");
281
- Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"));
282
- }
283
-
284
- [Test]
285
- public void MigrateSyntheticOwnerEntries_PartialDiskState_LeavesPlannedDllsMissingFromManifestEntry()
286
- {
287
- // Regression for the alreadyOnDisk gate: after MigrateSyntheticOwnerEntries
288
- // pulls only the synthetic entries that the disaster-recovery rebuild saw on
289
- // disk (a strict subset of the package's planned DLLs), the resulting
290
- // real-owner entry must NOT advertise the missing DLLs. NuGetPackageInstaller
291
- // gates `alreadyOnDisk` on `planned ⊆ manifestEntry.Dlls`, so an incomplete
292
- // migration here MUST yield an entry whose Dlls set fails that superset
293
- // check — otherwise extraction would be skipped and the missing files would
294
- // never be re-extracted.
295
- //
296
- // Scenario: Microsoft.Bcl.Memory ships 3 DLLs; only 2 survived on disk
297
- // (manifest deleted + one DLL lost to AV / partial cleanup). The user
298
- // re-runs at the same package version. The post-migration manifest entry
299
- // covers only the 2 surviving stems; planned still has 3.
300
- File.WriteAllText(Path.Combine(_installPath, "System.Memory.10.0.3.dll"), "dummy");
301
- File.WriteAllText(Path.Combine(_installPath, "System.Buffers.10.0.3.dll"), "dummy");
302
- // System.Runtime.CompilerServices.Unsafe.10.0.3.dll intentionally NOT seeded.
303
-
304
- var manifest = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
305
- Assert.IsTrue(manifest.Packages.ContainsKey("System.Memory"));
306
- Assert.IsTrue(manifest.Packages.ContainsKey("System.Buffers"));
307
- Assert.IsFalse(manifest.Packages.ContainsKey("System.Runtime.CompilerServices.Unsafe"),
308
- "Sanity: the missing DLL must not appear in the rebuilt manifest.");
309
-
310
- var planned = new System.Collections.Generic.List<PlannedDll>
311
- {
312
- new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", Path.Combine(_installPath, "System.Memory.10.0.3.dll")),
313
- new PlannedDll("lib/net8.0/System.Buffers.dll", "System.Buffers.10.0.3.dll", Path.Combine(_installPath, "System.Buffers.10.0.3.dll")),
314
- new PlannedDll("lib/net8.0/System.Runtime.CompilerServices.Unsafe.dll", "System.Runtime.CompilerServices.Unsafe.10.0.3.dll", Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll")),
315
- };
316
-
317
- var migrated = NuGetPackageInstaller.MigrateSyntheticOwnerEntries(
318
- manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
319
-
320
- Assert.IsTrue(migrated, "Migration must signal that the manifest changed (caller must persist).");
321
-
322
- Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"));
323
- var realEntry = manifest.Packages["Microsoft.Bcl.Memory"];
324
- CollectionAssert.AreEquivalent(
325
- new[] { "System.Memory.10.0.3.dll", "System.Buffers.10.0.3.dll" },
326
- realEntry.Dlls,
327
- "Migrated entry must reflect on-disk reality (2 of 3 DLLs), not the full planned set.");
328
-
329
- // Cross-check the gate the installer uses: planned ⊄ realEntry.Dlls, so
330
- // alreadyOnDisk would return false and the missing DLL gets re-extracted.
331
- var allPlannedRecorded = planned.TrueForAll(
332
- p => realEntry.Dlls.Contains(p.FileName, System.StringComparer.OrdinalIgnoreCase));
333
- Assert.IsFalse(allPlannedRecorded,
334
- "alreadyOnDisk gate would short-circuit incorrectly if the planned set were a subset of the migrated entry.");
335
- }
336
-
337
- [Test]
338
- public void MigrateSyntheticOwnerEntries_ReturnsFalse_WhenNothingMigrated()
339
- {
340
- // Caller persists only on a true return — ensure we don't churn the manifest
341
- // file when the migrator was a no-op.
342
- var manifest = new InstallManifest();
343
- var unrelatedEntry = new InstalledPackage("9.9.9");
344
- unrelatedEntry.Dlls.Add("Unrelated.9.9.9.dll");
345
- manifest.Packages["Unrelated"] = unrelatedEntry;
346
-
347
- var planned = new System.Collections.Generic.List<PlannedDll>
348
- {
349
- new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", "/ignored/System.Memory.10.0.3.dll"),
350
- };
351
-
352
- var migrated = NuGetPackageInstaller.MigrateSyntheticOwnerEntries(
353
- manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
354
-
355
- Assert.IsFalse(migrated);
356
- }
357
-
358
- [Test]
359
- public void MigrateSyntheticOwnerEntries_IgnoresEntriesAtDifferentVersion()
360
- {
361
- // A stem-keyed entry left at a DIFFERENT version is not part of the
362
- // reconciliation scope; it gets cleaned up by the stale-version
363
- // pass instead.
364
- var manifest = new InstallManifest();
365
- var olderSynthetic = new InstalledPackage("9.0.0");
366
- olderSynthetic.Dlls.Add("System.Memory.9.0.0.dll");
367
- manifest.Packages["System.Memory"] = olderSynthetic;
368
-
369
- var planned = new System.Collections.Generic.List<PlannedDll>
370
- {
371
- new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", "/ignored/System.Memory.10.0.3.dll"),
372
- };
373
-
374
- NuGetPackageInstaller.MigrateSyntheticOwnerEntries(manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
375
-
376
- Assert.IsTrue(manifest.Packages.ContainsKey("System.Memory"),
377
- "Different-version synthetic entry must be left alone (stale-version cleanup handles it).");
378
- Assert.IsFalse(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"),
379
- "No real-owner entry should be created when nothing was migrated.");
380
- }
381
-
382
- [Test]
383
- public void TryParseInstalledDllName_GreedilyConsumesEntireVersionTail()
384
- {
385
- // Regression check on the parser used by the disaster-recovery
386
- // rebuild: for "System.Memory.10.0.3.dll" the version is "10.0.3"
387
- // (not "0.3" or "3"), and the stem is "System.Memory".
388
- Assert.IsTrue(NuGetInstallManifest.TryParseInstalledDllName(
389
- "System.Memory.10.0.3.dll", out var stem, out var version));
390
- Assert.AreEqual("System.Memory", stem);
391
- Assert.AreEqual("10.0.3", version);
392
- }
393
-
394
- [Test]
395
- public void TryParseInstalledDllName_HandlesPackageStemsWithDots()
396
- {
397
- Assert.IsTrue(NuGetInstallManifest.TryParseInstalledDllName(
398
- "Microsoft.Extensions.Logging.Abstractions.8.0.3.dll", out var stem, out var version));
399
- Assert.AreEqual("Microsoft.Extensions.Logging.Abstractions", stem);
400
- Assert.AreEqual("8.0.3", version);
401
- }
402
-
403
- [Test]
404
- public void TryParseInstalledDllName_HandlesShortStemAndLongVersion()
405
- {
406
- // "R3.1.3.0.dll" → stem "R3", version "1.3.0".
407
- Assert.IsTrue(NuGetInstallManifest.TryParseInstalledDllName(
408
- "R3.1.3.0.dll", out var stem, out var version));
409
- Assert.AreEqual("R3", stem);
410
- Assert.AreEqual("1.3.0", version);
411
- }
412
-
413
- [Test]
414
- public void TryParseInstalledDllName_RejectsLegacyUnversionedFilename()
415
- {
416
- // No version tail → not a flat-layout install entry.
417
- Assert.IsFalse(NuGetInstallManifest.TryParseInstalledDllName(
418
- "System.Memory.dll", out _, out _));
419
- }
420
-
421
- [Test]
422
- public void TryParseInstalledDllName_RejectsMalformedTail()
423
- {
424
- // ".bar" tail isn't a System.Version.
425
- Assert.IsFalse(NuGetInstallManifest.TryParseInstalledDllName(
426
- "Foo.bar.dll", out _, out _));
427
- }
428
-
429
- [Test]
430
- public void TryParseInstalledDllName_RejectsNonDllExtension()
431
- {
432
- Assert.IsFalse(NuGetInstallManifest.TryParseInstalledDllName(
433
- "System.Memory.10.0.3.exe", out _, out _));
434
- }
435
- }
436
- }
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.Linq;
14
+ using NUnit.Framework;
15
+ using com.AtelierAI.Unity.Copilot.Editor.DependencyResolver;
16
+
17
+ namespace com.AtelierAI.Unity.Copilot.Editor.Tests.DependencyResolverTests
18
+ {
19
+ /// <summary>
20
+ /// Coverage for the on-disk manifest <c>.nuget-installed.json</c> introduced
21
+ /// for the flat layout (issue #733). The manifest is the primary source of
22
+ /// truth for "which DLL belongs to which package at which version", with
23
+ /// versioned-filename parsing as the disaster-recovery fallback.
24
+ /// </summary>
25
+ [TestFixture]
26
+ public class NuGetInstallManifestTests
27
+ {
28
+ string _installPath = string.Empty;
29
+
30
+ [SetUp]
31
+ public void SetUp()
32
+ {
33
+ _installPath = Path.Combine(
34
+ Path.GetTempPath(),
35
+ "Uco-Manifest-" + Path.GetRandomFileName());
36
+ Directory.CreateDirectory(_installPath);
37
+ }
38
+
39
+ [TearDown]
40
+ public void TearDown()
41
+ {
42
+ if (Directory.Exists(_installPath))
43
+ {
44
+ try { Directory.Delete(_installPath, recursive: true); }
45
+ catch { /* best-effort cleanup */ }
46
+ }
47
+ }
48
+
49
+ [Test]
50
+ public void Load_ReturnsEmptyManifest_WhenFileMissing()
51
+ {
52
+ var manifest = NuGetInstallManifest.Load(_installPath);
53
+ Assert.AreEqual(0, manifest.Packages.Count);
54
+ }
55
+
56
+ [Test]
57
+ public void SaveThenLoad_RoundTrips_AllFields()
58
+ {
59
+ var manifest = new InstallManifest();
60
+ var entry = new InstalledPackage("8.0.1");
61
+ entry.Dlls.Add("Microsoft.Extensions.Logging.8.0.1.dll");
62
+ entry.Dlls.Add("Microsoft.Extensions.Logging.Abstractions.8.0.3.dll");
63
+ manifest.Packages["Microsoft.Extensions.Logging"] = entry;
64
+
65
+ // Multi-DLL package with a different version.
66
+ var multi = new InstalledPackage("10.0.3");
67
+ multi.Dlls.Add("System.Memory.10.0.3.dll");
68
+ multi.Dlls.Add("System.Buffers.10.0.3.dll");
69
+ manifest.Packages["Microsoft.Bcl.Memory"] = multi;
70
+
71
+ // Empty-DLL entry (development-only dependency).
72
+ manifest.Packages["Microsoft.CodeAnalysis.Analyzers"] = new InstalledPackage("3.11.0");
73
+
74
+ NuGetInstallManifest.Save(_installPath, manifest);
75
+
76
+ var roundTrip = NuGetInstallManifest.Load(_installPath);
77
+
78
+ Assert.AreEqual(3, roundTrip.Packages.Count);
79
+ Assert.AreEqual("8.0.1", roundTrip.Packages["Microsoft.Extensions.Logging"].Version);
80
+ Assert.AreEqual(2, roundTrip.Packages["Microsoft.Extensions.Logging"].Dlls.Count);
81
+ Assert.AreEqual("10.0.3", roundTrip.Packages["Microsoft.Bcl.Memory"].Version);
82
+ CollectionAssert.AreEquivalent(
83
+ new[] { "System.Memory.10.0.3.dll", "System.Buffers.10.0.3.dll" },
84
+ roundTrip.Packages["Microsoft.Bcl.Memory"].Dlls);
85
+ Assert.AreEqual("3.11.0", roundTrip.Packages["Microsoft.CodeAnalysis.Analyzers"].Version);
86
+ Assert.AreEqual(0, roundTrip.Packages["Microsoft.CodeAnalysis.Analyzers"].Dlls.Count);
87
+ }
88
+
89
+ [Test]
90
+ public void Save_CreatesInstallDirectoryIfMissing()
91
+ {
92
+ var nested = Path.Combine(_installPath, "nested-not-yet-created");
93
+ Assert.IsFalse(Directory.Exists(nested));
94
+
95
+ var manifest = new InstallManifest();
96
+ NuGetInstallManifest.Save(nested, manifest);
97
+
98
+ Assert.IsTrue(Directory.Exists(nested));
99
+ Assert.IsTrue(File.Exists(Path.Combine(nested, ".nuget-installed.json")));
100
+ }
101
+
102
+ [Test]
103
+ public void Load_ReturnsEmptyManifest_OnMalformedJson_AndDoesNotThrow()
104
+ {
105
+ File.WriteAllText(Path.Combine(_installPath, ".nuget-installed.json"), "{ not valid json");
106
+
107
+ var manifest = NuGetInstallManifest.Load(_installPath);
108
+
109
+ Assert.AreEqual(0, manifest.Packages.Count);
110
+ }
111
+
112
+ [Test]
113
+ public void Load_PreservesCaseInsensitiveLookup()
114
+ {
115
+ // The runtime resolver matches package IDs case-insensitively
116
+ // throughout. The manifest must round-trip with the same property.
117
+ var manifest = new InstallManifest();
118
+ manifest.Packages["System.Text.Json"] = new InstalledPackage("8.0.5");
119
+ NuGetInstallManifest.Save(_installPath, manifest);
120
+
121
+ var loaded = NuGetInstallManifest.Load(_installPath);
122
+ Assert.IsTrue(loaded.Packages.ContainsKey("system.text.json"),
123
+ "Package ID lookup must be case-insensitive after a round-trip.");
124
+ Assert.IsTrue(loaded.Packages.ContainsKey("SYSTEM.TEXT.JSON"));
125
+ }
126
+
127
+ [Test]
128
+ public void Save_IsIdempotent_RoundTripIsByteForByte()
129
+ {
130
+ // Two saves of the same logical manifest must produce the exact
131
+ // same bytes — keeps git diffs clean and avoids spurious
132
+ // post-restore changes.
133
+ var manifest = new InstallManifest();
134
+ var entry = new InstalledPackage("8.0.5");
135
+ entry.Dlls.Add("System.Text.Json.8.0.5.dll");
136
+ manifest.Packages["System.Text.Json"] = entry;
137
+
138
+ NuGetInstallManifest.Save(_installPath, manifest);
139
+ var first = File.ReadAllBytes(Path.Combine(_installPath, ".nuget-installed.json"));
140
+
141
+ // Round-trip and save again.
142
+ var loaded = NuGetInstallManifest.Load(_installPath);
143
+ NuGetInstallManifest.Save(_installPath, loaded);
144
+ var second = File.ReadAllBytes(Path.Combine(_installPath, ".nuget-installed.json"));
145
+
146
+ CollectionAssert.AreEqual(first, second);
147
+ }
148
+
149
+ [Test]
150
+ public void TryRebuildFromDisk_ReproducesSingleDllPackagesFromVersionedFilenames()
151
+ {
152
+ // Disaster recovery (#733 acceptance criterion): user deletes
153
+ // .nuget-installed.json. The next restore must rebuild the manifest
154
+ // from on-disk versioned filenames, with no re-extraction needed.
155
+ File.WriteAllText(Path.Combine(_installPath, "Microsoft.Extensions.Logging.8.0.1.dll"), "dummy");
156
+ File.WriteAllText(Path.Combine(_installPath, "System.Text.Json.8.0.5.dll"), "dummy");
157
+ File.WriteAllText(Path.Combine(_installPath, "R3.1.3.0.dll"), "dummy");
158
+ // Plus an unrelated file the rebuild must ignore.
159
+ File.WriteAllText(Path.Combine(_installPath, "ReadMe.txt"), "user notes");
160
+
161
+ var rebuilt = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
162
+
163
+ Assert.AreEqual(3, rebuilt.Packages.Count);
164
+ Assert.AreEqual("8.0.1", rebuilt.Packages["Microsoft.Extensions.Logging"].Version);
165
+ Assert.AreEqual("8.0.5", rebuilt.Packages["System.Text.Json"].Version);
166
+ Assert.AreEqual("1.3.0", rebuilt.Packages["R3"].Version);
167
+ }
168
+
169
+ [Test]
170
+ public void TryRebuildFromDisk_IgnoresLegacyUnversionedDllsAndNonDllFiles()
171
+ {
172
+ // Pre-flat-layout artifacts the user might still have on disk —
173
+ // the parser must reject them so the rebuild stays consistent.
174
+ File.WriteAllText(Path.Combine(_installPath, "System.Memory.dll"), "legacy unversioned");
175
+ File.WriteAllText(Path.Combine(_installPath, "ReadMe.md"), "notes");
176
+
177
+ var rebuilt = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
178
+
179
+ Assert.AreEqual(0, rebuilt.Packages.Count);
180
+ }
181
+
182
+ [Test]
183
+ public void TryRebuildFromDisk_KeysMultiDllPackagesUnderSyntheticStems()
184
+ {
185
+ // Microsoft.Bcl.Memory is the canonical multi-DLL package: it ships
186
+ // System.Memory, System.Buffers, System.Runtime.CompilerServices.Unsafe.
187
+ // The disaster-recovery rebuild has no way to recover the real owner
188
+ // from filenames alone, so it MUST key those DLLs under their own
189
+ // stems as synthetic IDs. The follow-up MigrateSyntheticOwnerEntries
190
+ // call (driven from NuGetPackageInstaller.Install) reconciles the
191
+ // synthetic IDs back onto the real package ID — see the
192
+ // ReconcilesSyntheticEntries_ForMultiDllPackage test below.
193
+ File.WriteAllText(Path.Combine(_installPath, "System.Memory.10.0.3.dll"), "dummy");
194
+ File.WriteAllText(Path.Combine(_installPath, "System.Buffers.10.0.3.dll"), "dummy");
195
+ File.WriteAllText(Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll"), "dummy");
196
+
197
+ var rebuilt = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
198
+
199
+ Assert.AreEqual(3, rebuilt.Packages.Count);
200
+ Assert.IsTrue(rebuilt.Packages.ContainsKey("System.Memory"));
201
+ Assert.IsTrue(rebuilt.Packages.ContainsKey("System.Buffers"));
202
+ Assert.IsTrue(rebuilt.Packages.ContainsKey("System.Runtime.CompilerServices.Unsafe"));
203
+ Assert.IsFalse(rebuilt.Packages.ContainsKey("Microsoft.Bcl.Memory"),
204
+ "Filename-only rebuild cannot recover the real owning package id; the test guards the synthetic-id contract that MigrateSyntheticOwnerEntries depends on.");
205
+ }
206
+
207
+ [Test]
208
+ public void MigrateSyntheticOwnerEntries_ReconcilesMultiDllPackage_AndDoesNotTripCollisionCheck()
209
+ {
210
+ // End-to-end coverage of the post-rebuild reconciliation flow:
211
+ // 1. Seed a multi-DLL package's flat-layout DLLs without a manifest.
212
+ // 2. Run TryRebuildFromDisk — expect synthetic stem-keyed entries.
213
+ // 3. Run MigrateSyntheticOwnerEntries with the real package id and a
214
+ // planned-DLL list that matches the on-disk filenames.
215
+ // 4. Assert the synthetic entries are gone and the real package id
216
+ // now owns those same DLLs at the same version.
217
+ // Without this fix, NuGetPackageInstaller.Install would log
218
+ // "Refusing to install Microsoft.Bcl.Memory ..." and stick the user
219
+ // on the disaster-recovery path forever.
220
+ File.WriteAllText(Path.Combine(_installPath, "System.Memory.10.0.3.dll"), "dummy");
221
+ File.WriteAllText(Path.Combine(_installPath, "System.Buffers.10.0.3.dll"), "dummy");
222
+ File.WriteAllText(Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll"), "dummy");
223
+
224
+ var manifest = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
225
+ // Sanity: the rebuild produced synthetic entries (the [high] reproduction state).
226
+ Assert.IsTrue(manifest.Packages.ContainsKey("System.Memory"));
227
+ Assert.IsFalse(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"));
228
+
229
+ var planned = new System.Collections.Generic.List<PlannedDll>
230
+ {
231
+ new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", Path.Combine(_installPath, "System.Memory.10.0.3.dll")),
232
+ new PlannedDll("lib/net8.0/System.Buffers.dll", "System.Buffers.10.0.3.dll", Path.Combine(_installPath, "System.Buffers.10.0.3.dll")),
233
+ new PlannedDll("lib/net8.0/System.Runtime.CompilerServices.Unsafe.dll", "System.Runtime.CompilerServices.Unsafe.10.0.3.dll", Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll")),
234
+ };
235
+
236
+ NuGetPackageInstaller.MigrateSyntheticOwnerEntries(manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
237
+
238
+ Assert.IsFalse(manifest.Packages.ContainsKey("System.Memory"),
239
+ "Synthetic stem-keyed entry must be removed after reconciliation.");
240
+ Assert.IsFalse(manifest.Packages.ContainsKey("System.Buffers"));
241
+ Assert.IsFalse(manifest.Packages.ContainsKey("System.Runtime.CompilerServices.Unsafe"));
242
+ Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"),
243
+ "Real package id must own the migrated DLLs after reconciliation.");
244
+ Assert.AreEqual("10.0.3", manifest.Packages["Microsoft.Bcl.Memory"].Version);
245
+ CollectionAssert.AreEquivalent(
246
+ new[]
247
+ {
248
+ "System.Memory.10.0.3.dll",
249
+ "System.Buffers.10.0.3.dll",
250
+ "System.Runtime.CompilerServices.Unsafe.10.0.3.dll",
251
+ },
252
+ manifest.Packages["Microsoft.Bcl.Memory"].Dlls);
253
+ }
254
+
255
+ [Test]
256
+ public void MigrateSyntheticOwnerEntries_DoesNotTouchUnrelatedPackages()
257
+ {
258
+ // Defense: the migrator must only strip entries whose DLL set is a
259
+ // subset of the planned filenames. A real distinct package that
260
+ // happens to share a version number must survive untouched.
261
+ var manifest = new InstallManifest();
262
+ var unrelatedEntry = new InstalledPackage("10.0.3");
263
+ unrelatedEntry.Dlls.Add("Newtonsoft.Json.10.0.3.dll");
264
+ manifest.Packages["Newtonsoft.Json"] = unrelatedEntry;
265
+
266
+ var syntheticEntry = new InstalledPackage("10.0.3");
267
+ syntheticEntry.Dlls.Add("System.Memory.10.0.3.dll");
268
+ manifest.Packages["System.Memory"] = syntheticEntry;
269
+
270
+ var planned = new System.Collections.Generic.List<PlannedDll>
271
+ {
272
+ new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", "/ignored/System.Memory.10.0.3.dll"),
273
+ };
274
+
275
+ NuGetPackageInstaller.MigrateSyntheticOwnerEntries(manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
276
+
277
+ Assert.IsTrue(manifest.Packages.ContainsKey("Newtonsoft.Json"),
278
+ "Unrelated same-version package must not be migrated away.");
279
+ Assert.IsFalse(manifest.Packages.ContainsKey("System.Memory"),
280
+ "Synthetic same-version entry whose DLLs match the planned set must be migrated.");
281
+ Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"));
282
+ }
283
+
284
+ [Test]
285
+ public void MigrateSyntheticOwnerEntries_PartialDiskState_LeavesPlannedDllsMissingFromManifestEntry()
286
+ {
287
+ // Regression for the alreadyOnDisk gate: after MigrateSyntheticOwnerEntries
288
+ // pulls only the synthetic entries that the disaster-recovery rebuild saw on
289
+ // disk (a strict subset of the package's planned DLLs), the resulting
290
+ // real-owner entry must NOT advertise the missing DLLs. NuGetPackageInstaller
291
+ // gates `alreadyOnDisk` on `planned ⊆ manifestEntry.Dlls`, so an incomplete
292
+ // migration here MUST yield an entry whose Dlls set fails that superset
293
+ // check — otherwise extraction would be skipped and the missing files would
294
+ // never be re-extracted.
295
+ //
296
+ // Scenario: Microsoft.Bcl.Memory ships 3 DLLs; only 2 survived on disk
297
+ // (manifest deleted + one DLL lost to AV / partial cleanup). The user
298
+ // re-runs at the same package version. The post-migration manifest entry
299
+ // covers only the 2 surviving stems; planned still has 3.
300
+ File.WriteAllText(Path.Combine(_installPath, "System.Memory.10.0.3.dll"), "dummy");
301
+ File.WriteAllText(Path.Combine(_installPath, "System.Buffers.10.0.3.dll"), "dummy");
302
+ // System.Runtime.CompilerServices.Unsafe.10.0.3.dll intentionally NOT seeded.
303
+
304
+ var manifest = NuGetInstallManifest.TryRebuildFromDisk(_installPath);
305
+ Assert.IsTrue(manifest.Packages.ContainsKey("System.Memory"));
306
+ Assert.IsTrue(manifest.Packages.ContainsKey("System.Buffers"));
307
+ Assert.IsFalse(manifest.Packages.ContainsKey("System.Runtime.CompilerServices.Unsafe"),
308
+ "Sanity: the missing DLL must not appear in the rebuilt manifest.");
309
+
310
+ var planned = new System.Collections.Generic.List<PlannedDll>
311
+ {
312
+ new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", Path.Combine(_installPath, "System.Memory.10.0.3.dll")),
313
+ new PlannedDll("lib/net8.0/System.Buffers.dll", "System.Buffers.10.0.3.dll", Path.Combine(_installPath, "System.Buffers.10.0.3.dll")),
314
+ new PlannedDll("lib/net8.0/System.Runtime.CompilerServices.Unsafe.dll", "System.Runtime.CompilerServices.Unsafe.10.0.3.dll", Path.Combine(_installPath, "System.Runtime.CompilerServices.Unsafe.10.0.3.dll")),
315
+ };
316
+
317
+ var migrated = NuGetPackageInstaller.MigrateSyntheticOwnerEntries(
318
+ manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
319
+
320
+ Assert.IsTrue(migrated, "Migration must signal that the manifest changed (caller must persist).");
321
+
322
+ Assert.IsTrue(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"));
323
+ var realEntry = manifest.Packages["Microsoft.Bcl.Memory"];
324
+ CollectionAssert.AreEquivalent(
325
+ new[] { "System.Memory.10.0.3.dll", "System.Buffers.10.0.3.dll" },
326
+ realEntry.Dlls,
327
+ "Migrated entry must reflect on-disk reality (2 of 3 DLLs), not the full planned set.");
328
+
329
+ // Cross-check the gate the installer uses: planned ⊄ realEntry.Dlls, so
330
+ // alreadyOnDisk would return false and the missing DLL gets re-extracted.
331
+ var allPlannedRecorded = planned.TrueForAll(
332
+ p => realEntry.Dlls.Contains(p.FileName, System.StringComparer.OrdinalIgnoreCase));
333
+ Assert.IsFalse(allPlannedRecorded,
334
+ "alreadyOnDisk gate would short-circuit incorrectly if the planned set were a subset of the migrated entry.");
335
+ }
336
+
337
+ [Test]
338
+ public void MigrateSyntheticOwnerEntries_ReturnsFalse_WhenNothingMigrated()
339
+ {
340
+ // Caller persists only on a true return — ensure we don't churn the manifest
341
+ // file when the migrator was a no-op.
342
+ var manifest = new InstallManifest();
343
+ var unrelatedEntry = new InstalledPackage("9.9.9");
344
+ unrelatedEntry.Dlls.Add("Unrelated.9.9.9.dll");
345
+ manifest.Packages["Unrelated"] = unrelatedEntry;
346
+
347
+ var planned = new System.Collections.Generic.List<PlannedDll>
348
+ {
349
+ new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", "/ignored/System.Memory.10.0.3.dll"),
350
+ };
351
+
352
+ var migrated = NuGetPackageInstaller.MigrateSyntheticOwnerEntries(
353
+ manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
354
+
355
+ Assert.IsFalse(migrated);
356
+ }
357
+
358
+ [Test]
359
+ public void MigrateSyntheticOwnerEntries_IgnoresEntriesAtDifferentVersion()
360
+ {
361
+ // A stem-keyed entry left at a DIFFERENT version is not part of the
362
+ // reconciliation scope; it gets cleaned up by the stale-version
363
+ // pass instead.
364
+ var manifest = new InstallManifest();
365
+ var olderSynthetic = new InstalledPackage("9.0.0");
366
+ olderSynthetic.Dlls.Add("System.Memory.9.0.0.dll");
367
+ manifest.Packages["System.Memory"] = olderSynthetic;
368
+
369
+ var planned = new System.Collections.Generic.List<PlannedDll>
370
+ {
371
+ new PlannedDll("lib/net8.0/System.Memory.dll", "System.Memory.10.0.3.dll", "/ignored/System.Memory.10.0.3.dll"),
372
+ };
373
+
374
+ NuGetPackageInstaller.MigrateSyntheticOwnerEntries(manifest, "Microsoft.Bcl.Memory", "10.0.3", planned);
375
+
376
+ Assert.IsTrue(manifest.Packages.ContainsKey("System.Memory"),
377
+ "Different-version synthetic entry must be left alone (stale-version cleanup handles it).");
378
+ Assert.IsFalse(manifest.Packages.ContainsKey("Microsoft.Bcl.Memory"),
379
+ "No real-owner entry should be created when nothing was migrated.");
380
+ }
381
+
382
+ [Test]
383
+ public void TryParseInstalledDllName_GreedilyConsumesEntireVersionTail()
384
+ {
385
+ // Regression check on the parser used by the disaster-recovery
386
+ // rebuild: for "System.Memory.10.0.3.dll" the version is "10.0.3"
387
+ // (not "0.3" or "3"), and the stem is "System.Memory".
388
+ Assert.IsTrue(NuGetInstallManifest.TryParseInstalledDllName(
389
+ "System.Memory.10.0.3.dll", out var stem, out var version));
390
+ Assert.AreEqual("System.Memory", stem);
391
+ Assert.AreEqual("10.0.3", version);
392
+ }
393
+
394
+ [Test]
395
+ public void TryParseInstalledDllName_HandlesPackageStemsWithDots()
396
+ {
397
+ Assert.IsTrue(NuGetInstallManifest.TryParseInstalledDllName(
398
+ "Microsoft.Extensions.Logging.Abstractions.8.0.3.dll", out var stem, out var version));
399
+ Assert.AreEqual("Microsoft.Extensions.Logging.Abstractions", stem);
400
+ Assert.AreEqual("8.0.3", version);
401
+ }
402
+
403
+ [Test]
404
+ public void TryParseInstalledDllName_HandlesShortStemAndLongVersion()
405
+ {
406
+ // "R3.1.3.0.dll" → stem "R3", version "1.3.0".
407
+ Assert.IsTrue(NuGetInstallManifest.TryParseInstalledDllName(
408
+ "R3.1.3.0.dll", out var stem, out var version));
409
+ Assert.AreEqual("R3", stem);
410
+ Assert.AreEqual("1.3.0", version);
411
+ }
412
+
413
+ [Test]
414
+ public void TryParseInstalledDllName_RejectsLegacyUnversionedFilename()
415
+ {
416
+ // No version tail → not a flat-layout install entry.
417
+ Assert.IsFalse(NuGetInstallManifest.TryParseInstalledDllName(
418
+ "System.Memory.dll", out _, out _));
419
+ }
420
+
421
+ [Test]
422
+ public void TryParseInstalledDllName_RejectsMalformedTail()
423
+ {
424
+ // ".bar" tail isn't a System.Version.
425
+ Assert.IsFalse(NuGetInstallManifest.TryParseInstalledDllName(
426
+ "Foo.bar.dll", out _, out _));
427
+ }
428
+
429
+ [Test]
430
+ public void TryParseInstalledDllName_RejectsNonDllExtension()
431
+ {
432
+ Assert.IsFalse(NuGetInstallManifest.TryParseInstalledDllName(
433
+ "System.Memory.10.0.3.exe", out _, out _));
434
+ }
435
+ }
436
+ }