From 8fe9b32988528bc5b57cf346b22941425ed9ec3a Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 8 Jul 2026 21:36:24 +0900 Subject: [PATCH 1/3] refactor: centralize CLI-only path identity checks Route both current-project and CLI-only source-root comparisons through one platform-aware seam while preserving the existing ordinal comparison. Passing the runtime platform explicitly keeps the upcoming path identity policy free of hidden environment dependencies. --- .../SkillSetup/SkillSourceRootEnumerator.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs index 2d0da8f079..0f99fbfc17 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; } @@ -230,10 +230,21 @@ 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) { return string.Equals( - Path.GetFullPath(searchRoot), - Path.GetFullPath(GetCliOnlySkillSourceRoot(UnityCliLoopPathResolver.GetProjectRoot())), + Path.GetFullPath(leftPath), + Path.GetFullPath(rightPath), StringComparison.Ordinal); } From 9d83edf01dda7d8b58716da5eb0aec214b16cece Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 8 Jul 2026 21:38:18 +0900 Subject: [PATCH 2/3] fix: match CLI-only roots using platform path identity Treat differently cased paths as identical for Windows and macOS editor sessions while retaining ordinal identity on Linux. This prevents the CLI-only source gate from dropping equivalent current-project paths on the default case-insensitive filesystems. Add a deterministic three-platform contract test. Per-volume filesystem probing is intentionally avoided because it would add I/O and complexity to a narrow source-discovery gate. --- .../Tests/Editor/SkillInstallLayoutTests.cs | 19 +++++++++++++++++++ .../SkillSetup/SkillSourceRootEnumerator.cs | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/SkillInstallLayoutTests.cs b/Assets/Tests/Editor/SkillInstallLayoutTests.cs index 3768015446..1d483bdc2f 100644 --- a/Assets/Tests/Editor/SkillInstallLayoutTests.cs +++ b/Assets/Tests/Editor/SkillInstallLayoutTests.cs @@ -121,6 +121,25 @@ 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 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 0f99fbfc17..1ee1f79152 100644 --- a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs +++ b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs @@ -242,10 +242,17 @@ internal static bool HaveSamePathIdentityAtPlatform( 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(leftPath), Path.GetFullPath(rightPath), - StringComparison.Ordinal); + comparison); } private static IEnumerable EnumerateDirectProjectPackageRoots(string projectRoot) From baf2d4d4614be29cda0598ecf766e9279aaff037 Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 8 Jul 2026 21:47:45 +0900 Subject: [PATCH 3/3] fix: normalize trailing separators in path identity Path.GetFullPath preserves trailing directory separators, so equivalent project roots could still fail the centralized identity check. Normalize optional trailing separators while retaining filesystem roots such as slash and drive roots. Add three-platform regression coverage for trailing-separator equivalence. --- .../Tests/Editor/SkillInstallLayoutTests.cs | 18 ++++++++++++++++ .../SkillSetup/SkillSourceRootEnumerator.cs | 21 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Assets/Tests/Editor/SkillInstallLayoutTests.cs b/Assets/Tests/Editor/SkillInstallLayoutTests.cs index 1d483bdc2f..a2e2188ee8 100644 --- a/Assets/Tests/Editor/SkillInstallLayoutTests.cs +++ b/Assets/Tests/Editor/SkillInstallLayoutTests.cs @@ -140,6 +140,24 @@ public void HaveSamePathIdentityAtPlatform_WhenCasingDiffers_UsesPlatformPolicy( 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 1ee1f79152..7d53da1ff0 100644 --- a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs +++ b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs @@ -250,11 +250,28 @@ internal static bool HaveSamePathIdentityAtPlatform( ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; return string.Equals( - Path.GetFullPath(leftPath), - Path.GetFullPath(rightPath), + 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) { string packagesRoot = Path.Combine(projectRoot, "Packages");