using System.Linq;
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor;
using Unity.Pipeline.Editor.Commands.PackageManager;
namespace Unity.Pipeline.Tests.Editor
{
///
/// Tests for the Package Manager commands (CLI-203). Coverage is the parts that don't require a
/// live UPM round-trip: identifier classification, manifest parsing, command discovery + gate
/// schema, and the confirm/dry-run gate on the mutating add/remove paths (which is enforced before
/// any Client call, so it never touches the real project). A confirmed add/remove would mutate the
/// manifest and force a domain reload, so it is intentionally not exercised here.
///
public class PackageManagerCommandTests
{
[SetUp]
public void SetUp()
{
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
}
// ---- Identifier classification ---------------------------------------------------------
[TestCase("com.unity.foo", PackageSourceKind.Registry)]
[TestCase("com.unity.foo@1.2.3", PackageSourceKind.Registry)]
[TestCase("file:../MyLocalPackage", PackageSourceKind.Local)]
[TestCase("https://github.com/user/repo.git", PackageSourceKind.Git)]
[TestCase("https://github.com/user/repo.git#v1.0.0", PackageSourceKind.Git)]
[TestCase("git+https://github.com/user/repo.git", PackageSourceKind.Git)]
[TestCase("ssh://git@github.com/user/repo.git", PackageSourceKind.Git)]
[TestCase("git@github.com:user/repo.git", PackageSourceKind.Git)]
[TestCase("", PackageSourceKind.Unknown)]
public void Classify_DetectsSource(string identifier, PackageSourceKind expected)
{
Assert.AreEqual(expected, PackageIdentifier.Classify(identifier));
}
[Test]
public void TryParse_RegistryWithVersion_SplitsNameAndVersion()
{
Assert.IsTrue(PackageIdentifier.TryParse("com.unity.foo@1.2.3", out var parsed, out var error), error);
Assert.AreEqual(PackageSourceKind.Registry, parsed.Kind);
Assert.AreEqual("com.unity.foo", parsed.Name);
Assert.AreEqual("1.2.3", parsed.Version);
Assert.AreEqual("com.unity.foo@1.2.3", parsed.Identifier);
}
[Test]
public void TryParse_RegistryWithoutVersion_HasNullVersion()
{
Assert.IsTrue(PackageIdentifier.TryParse("com.unity.foo", out var parsed, out _));
Assert.AreEqual("com.unity.foo", parsed.Name);
Assert.IsNull(parsed.Version);
}
[Test]
public void TryParse_GitWithRevision_ExtractsRevision()
{
Assert.IsTrue(PackageIdentifier.TryParse("https://github.com/user/repo.git#v1.0.0", out var parsed, out _));
Assert.AreEqual(PackageSourceKind.Git, parsed.Kind);
Assert.AreEqual("v1.0.0", parsed.Version);
}
[Test]
public void TryParse_Empty_Fails()
{
Assert.IsFalse(PackageIdentifier.TryParse(" ", out _, out var error));
StringAssert.Contains("empty", error);
}
// ---- Manifest parsing ------------------------------------------------------------------
[Test]
public void ReadDependencies_ParsesNameVersionMap()
{
const string json = @"{
""dependencies"": {
""com.unity.foo"": ""1.2.3"",
""com.unity.bar"": ""file:../bar""
}
}";
var deps = PackageManifest.ReadDependencies(json);
Assert.AreEqual(2, deps.Count);
Assert.AreEqual("1.2.3", deps["com.unity.foo"]);
Assert.AreEqual("file:../bar", deps["com.unity.bar"]);
}
[Test]
public void ReadDependencies_EmptyOrMissing_ReturnsEmptyMap()
{
Assert.AreEqual(0, PackageManifest.ReadDependencies("").Count);
Assert.AreEqual(0, PackageManifest.ReadDependencies("{}").Count);
}
// ---- Discovery + schema ----------------------------------------------------------------
[Test]
public void AllPackageCommands_AreDiscovered()
{
var names = CommandRegistry.DiscoverCommands().Select(c => c.Name).ToList();
foreach (var command in new[]
{
"package_list", "package_search", "package_add",
"package_remove", "package_resolve", "package_status"
})
{
Assert.Contains(command, names, $"missing command {command}");
}
}
[Test]
public void PackageList_InstalledScope_ReturnsResultsSynchronously()
{
// Default (installed) scope reads the resolved set directly — no async trigger / poll.
var response = (PackageListResponse)PackageManagerCommand.PackageList();
Assert.IsTrue(response.Success);
Assert.AreEqual("installed", response.Scope);
Assert.Greater(response.Count, 0, "a Unity project always has at least its built-in packages");
Assert.AreEqual(response.Count, response.Packages.Count);
Assert.IsNotNull(response.Manifest, "the manifest state should be included");
Assert.IsTrue(response.Packages.TrueForAll(p => p.IsInstalled), "installed scope: every entry is installed");
}
[Test]
public void PackageList_DirectDependenciesOnly_AreFewerThanAll()
{
var all = (PackageListResponse)PackageManagerCommand.PackageList("installed", includeIndirect: true);
var direct = (PackageListResponse)PackageManagerCommand.PackageList("installed", includeIndirect: false);
Assert.LessOrEqual(direct.Count, all.Count);
Assert.IsTrue(direct.Packages.TrueForAll(p => p.IsDirectDependency), "include_indirect=false should drop transitive deps");
}
[Test]
public void PackageList_UnknownScope_Fails()
{
var response = (PackageListResponse)PackageManagerCommand.PackageList("bogus");
Assert.IsFalse(response.Success);
StringAssert.Contains("scope", response.Message);
}
[Test]
public void PackageStatus_IsOffMainThread()
{
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "package_status");
Assert.IsFalse(command.MainThreadRequired, "package_status only reads a file; it must respond while an op is in flight");
}
[Test]
public void PackageList_ExposesScopeArg()
{
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "package_list");
var schema = JObject.Parse(JsonSchemaGenerator.GenerateCommandSchema(command));
var properties = (JObject)schema["properties"];
Assert.AreEqual("string", properties["scope"]?["type"]?.ToString());
Assert.AreEqual("boolean", properties["include_indirect"]?["type"]?.ToString());
}
[Test]
public void PackageSearch_ExposesQueryArg()
{
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "package_search");
var schema = JObject.Parse(JsonSchemaGenerator.GenerateCommandSchema(command));
var properties = (JObject)schema["properties"];
Assert.AreEqual("string", properties["query"]?["type"]?.ToString());
Assert.AreEqual("boolean", properties["offline"]?["type"]?.ToString());
}
[Test]
public void PackageAdd_ExposesGateArgs()
{
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "package_add");
var schema = JObject.Parse(JsonSchemaGenerator.GenerateCommandSchema(command));
var properties = (JObject)schema["properties"];
Assert.AreEqual("string", properties["identifier"]?["type"]?.ToString());
Assert.AreEqual("boolean", properties["confirm"]?["type"]?.ToString());
Assert.AreEqual("boolean", properties["dry_run"]?["type"]?.ToString());
Assert.AreEqual("boolean", properties["wait"]?["type"]?.ToString(), "add should expose the dual-mode wait flag");
var required = schema["required"]?.ToObject();
CollectionAssert.Contains(required, "identifier");
}
// ---- Gate behaviour (no live UPM op) ---------------------------------------------------
[Test]
public void PackageAdd_InvalidIdentifier_Fails()
{
var response = (PackageMutationResponse)PackageManagerCommand.PackageAdd("", confirm: true);
Assert.IsFalse(response.Success);
Assert.AreEqual("failed", response.Status);
}
[Test]
public void PackageAdd_WithoutConfirm_IsRejected()
{
// confirm=false → the command refuses before Client.Add is ever called.
var response = (PackageMutationResponse)PackageManagerCommand.PackageAdd("com.unity.foo@1.2.3", confirm: false);
Assert.IsFalse(response.Success);
Assert.AreEqual("rejected", response.Status);
Assert.IsFalse(response.Applied);
}
[Test]
public void PackageAdd_DryRun_PreviewsWithoutApplying()
{
var response = (PackageMutationResponse)PackageManagerCommand.PackageAdd("com.unity.foo@1.2.3", confirm: false, dryRun: true);
Assert.IsTrue(response.Success);
Assert.IsTrue(response.DryRun);
Assert.AreEqual("dry_run", response.Status);
Assert.IsFalse(response.Applied);
StringAssert.Contains("com.unity.foo", response.Plan);
}
[Test]
public void PackageRemove_EmptyName_Fails()
{
var response = (PackageMutationResponse)PackageManagerCommand.PackageRemove("", confirm: true);
Assert.IsFalse(response.Success);
Assert.AreEqual("failed", response.Status);
}
[Test]
public void PackageRemove_WithoutConfirm_IsRejected()
{
var response = (PackageMutationResponse)PackageManagerCommand.PackageRemove("com.unity.foo", confirm: false);
Assert.IsFalse(response.Success);
Assert.AreEqual("rejected", response.Status);
Assert.IsFalse(response.Applied);
}
}
}