diff --git a/Assets/Tests/Editor/SkillInstallLayoutTests.cs b/Assets/Tests/Editor/SkillInstallLayoutTests.cs index 3768015446..a2e2188ee8 100644 --- a/Assets/Tests/Editor/SkillInstallLayoutTests.cs +++ b/Assets/Tests/Editor/SkillInstallLayoutTests.cs @@ -121,6 +121,43 @@ public void GetSkillSourceInfos_WhenProjectIsCurrentRoot_IncludesCliOnlyCoreSkil Assert.That(skillSources.Select(skill => skill.Name), Does.Contain("uloop-launch")); } + // Tests that differently cased paths follow each editor platform's path identity policy. + [TestCase(UnityEngine.RuntimePlatform.WindowsEditor, true)] + [TestCase(UnityEngine.RuntimePlatform.OSXEditor, true)] + [TestCase(UnityEngine.RuntimePlatform.LinuxEditor, false)] + public void HaveSamePathIdentityAtPlatform_WhenCasingDiffers_UsesPlatformPolicy( + UnityEngine.RuntimePlatform platform, + bool expected) + { + string lowerCasePath = Path.Combine(_projectRoot, "case-sensitive-root"); + string upperCasePath = Path.Combine(_projectRoot, "CASE-SENSITIVE-ROOT"); + + bool actual = SkillSourceRootEnumerator.HaveSamePathIdentityAtPlatform( + lowerCasePath, + upperCasePath, + platform); + + Assert.That(actual, Is.EqualTo(expected)); + } + + // Tests that an optional trailing directory separator does not change path identity. + [TestCase(UnityEngine.RuntimePlatform.WindowsEditor)] + [TestCase(UnityEngine.RuntimePlatform.OSXEditor)] + [TestCase(UnityEngine.RuntimePlatform.LinuxEditor)] + public void HaveSamePathIdentityAtPlatform_WhenTrailingSeparatorDiffers_ReturnsTrue( + UnityEngine.RuntimePlatform platform) + { + string path = Path.Combine(_projectRoot, "skill-source-root"); + string pathWithTrailingSeparator = path + Path.DirectorySeparatorChar; + + bool actual = SkillSourceRootEnumerator.HaveSamePathIdentityAtPlatform( + path, + pathWithTrailingSeparator, + platform); + + Assert.That(actual, Is.True); + } + // Tests that Tool Settings can read source skill frontmatter descriptions by tool name. [Test] public void GetToolDescriptionsByToolName_WhenSkillHasDescription_MapsDescriptionToToolName() diff --git a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs index 2d0da8f079..7d53da1ff0 100644 --- a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs +++ b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs @@ -215,10 +215,10 @@ private static void AddSkillSourceRoot( private static string GetCliOnlySkillSourceRoot(string projectRoot) { string currentProjectRoot = UnityCliLoopPathResolver.GetProjectRoot(); - if (!string.Equals( - Path.GetFullPath(projectRoot), - Path.GetFullPath(currentProjectRoot), - StringComparison.Ordinal)) + if (!HaveSamePathIdentityAtPlatform( + projectRoot, + currentProjectRoot, + UnityEngine.Application.platform)) { return null; } @@ -231,10 +231,45 @@ private static string GetCliOnlySkillSourceRoot(string projectRoot) private static bool IsCliOnlySkillSourceRoot(string searchRoot) { + return HaveSamePathIdentityAtPlatform( + searchRoot, + GetCliOnlySkillSourceRoot(UnityCliLoopPathResolver.GetProjectRoot()), + UnityEngine.Application.platform); + } + + internal static bool HaveSamePathIdentityAtPlatform( + string leftPath, + string rightPath, + RuntimePlatform platform) + { + // Windows and macOS editor paths follow their default case-insensitive filesystem behavior. + // Probing case sensitivity per volume would add I/O and complexity to this CLI-only source gate. + bool usesCaseInsensitivePathIdentity = platform == RuntimePlatform.WindowsEditor + || platform == RuntimePlatform.OSXEditor; + StringComparison comparison = usesCaseInsensitivePathIdentity + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; return string.Equals( - Path.GetFullPath(searchRoot), - Path.GetFullPath(GetCliOnlySkillSourceRoot(UnityCliLoopPathResolver.GetProjectRoot())), - StringComparison.Ordinal); + NormalizePathForIdentity(leftPath), + NormalizePathForIdentity(rightPath), + comparison); + } + + private static string NormalizePathForIdentity(string path) + { + string fullPath = Path.GetFullPath(path); + string root = Path.GetPathRoot(fullPath); + if (!string.IsNullOrEmpty(root) && string.Equals(fullPath, root, StringComparison.OrdinalIgnoreCase)) + { + // Trimming the only separator from a filesystem root would change its identity. + return root; + } + + string normalizedPath = fullPath.TrimEnd( + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar); + Debug.Assert(!string.IsNullOrEmpty(normalizedPath), "normalizedPath must not be empty"); + return normalizedPath; } private static IEnumerable EnumerateDirectProjectPackageRoots(string projectRoot)