From 6b49a30d06ec6c7ef106c1a0a1c05b61071786ee Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 8 Jul 2026 21:13:52 +0900 Subject: [PATCH] fix: exclude stale cache roots for local dependencies Local file: and path: manifest dependencies are authoritative skill package roots, but both C# and Go discovery also admitted same-name PackageCache entries. This could expose stale cache-only skills alongside the local package. Filter local dependencies from PackageCache candidate identities by reusing the existing local-path resolvers in both implementations. Add regression coverage for both dependency prefixes and refresh the shared release input stamps required by the common Go change. --- .../Tests/Editor/SkillInstallLayoutTests.cs | 32 +++++++++++++++++ .../SkillSetup/SkillSourceRootEnumerator.cs | 4 ++- cli/common/skillscan/packages.go | 5 ++- cli/common/skillscan/packages_test.go | 34 +++++++++++++++++++ cli/dispatcher/shared-inputs-stamp.json | 2 +- cli/project-runner/shared-inputs-stamp.json | 2 +- 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Assets/Tests/Editor/SkillInstallLayoutTests.cs b/Assets/Tests/Editor/SkillInstallLayoutTests.cs index ed22d6c08b..3768015446 100644 --- a/Assets/Tests/Editor/SkillInstallLayoutTests.cs +++ b/Assets/Tests/Editor/SkillInstallLayoutTests.cs @@ -169,6 +169,38 @@ public void GetSkillSourceInfos_WhenNamesOverlap_PrefersEarlierSourceRoot() Assert.That(skillSources["uloop-manifest-priority"].ToolName, Is.EqualTo("manifest-winner")); } + // Tests that local manifest dependencies do not also discover stale PackageCache-only skills. + [TestCase("file:")] + [TestCase("path:")] + public void GetSkillSourceInfos_WhenLocalDependencyHasStaleCache_ExcludesCacheOnlySkill( + string dependencyPrefix) + { + string temporaryRoot = CreateTemporaryProjectRoot(); + string localPackageRoot = CreateTemporaryProjectRoot(); + string cachePackageRoot = Path.Combine( + temporaryRoot, + "Library", + "PackageCache", + "com.example.local@1.0.0"); + WriteSourceSkill(localPackageRoot, "uloop-local-skill", "local-tool", "LocalTool"); + WriteSourceSkill(cachePackageRoot, "uloop-stale-cache-skill", "stale-tool", "StaleTool"); + + string manifestPath = Path.Combine(temporaryRoot, "Packages", "manifest.json"); + Directory.CreateDirectory(Path.GetDirectoryName(manifestPath)); + string portableLocalPackageRoot = localPackageRoot.Replace(Path.DirectorySeparatorChar, '/'); + File.WriteAllText( + manifestPath, + $"{{\"dependencies\":{{\"com.example.local\":" + + $"\"{dependencyPrefix}{portableLocalPackageRoot}\"}}}}"); + + string[] skillNames = SkillInstallLayout.GetSkillSourceInfos(temporaryRoot) + .Select(skill => skill.Name) + .ToArray(); + + Assert.That(skillNames, Does.Contain("uloop-local-skill")); + Assert.That(skillNames, Does.Not.Contain("uloop-stale-cache-skill")); + } + // Tests that skill discovery follows bundled tools after they move into the first-party plugin assembly. [Test] public void GetSkillSourceInfos_WhenFirstPartyToolIsUnderFirstPartyTools_IncludesToolSkill() diff --git a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs index c9d1bd4240..2d0da8f079 100644 --- a/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs +++ b/Packages/src/Editor/Infrastructure/SkillSetup/SkillSourceRootEnumerator.cs @@ -268,7 +268,9 @@ private static IEnumerable EnumerateManifestLocalPackageRoots(string pro private static IEnumerable EnumerateDependencyPackageCacheRoots(string projectRoot) { HashSet dependencyNames = new( - EnumerateManifestDependencies(projectRoot).Select(dependency => dependency.Key), + EnumerateManifestDependencies(projectRoot) + .Where(dependency => ResolveLocalDependencyPath(dependency.Value, projectRoot) == null) + .Select(dependency => dependency.Key), StringComparer.OrdinalIgnoreCase); if (dependencyNames.Count == 0) { diff --git a/cli/common/skillscan/packages.go b/cli/common/skillscan/packages.go index 0f3fa85d89..b5c20d4a61 100644 --- a/cli/common/skillscan/packages.go +++ b/cli/common/skillscan/packages.go @@ -166,7 +166,10 @@ func enumeratePackageCacheResults(projectRoot string) []PackageSearchResult { return []PackageSearchResult{} } dependencyNames := map[string]bool{} - for dependencyName := range dependencies { + for dependencyName, dependencyValue := range dependencies { + if resolveLocalDependencyPath(dependencyValue, projectRoot) != "" { + continue + } dependencyNames[strings.ToLower(dependencyName)] = true } packageCacheDir := filepath.Join(projectRoot, "Library", "PackageCache") diff --git a/cli/common/skillscan/packages_test.go b/cli/common/skillscan/packages_test.go index 29b4cf2129..ffe1000d95 100644 --- a/cli/common/skillscan/packages_test.go +++ b/cli/common/skillscan/packages_test.go @@ -47,6 +47,40 @@ func TestEnumerateSourceRootsUsesPackageSearchResults(t *testing.T) { } } +// Tests that local manifest dependencies exclude stale roots with the same identity from PackageCache. +func TestEnumeratePackageSearchResultsExcludesStaleCacheForLocalDependencies(t *testing.T) { + for _, dependencyPrefix := range []string{"file:", "path:"} { + t.Run(dependencyPrefix, func(t *testing.T) { + projectRoot := t.TempDir() + localPackageRoot := filepath.Join(t.TempDir(), "local-package") + if err := os.MkdirAll(localPackageRoot, 0o755); err != nil { + t.Fatalf("failed to create local package: %v", err) + } + staleCacheRoot := filepath.Join( + projectRoot, + "Library", + "PackageCache", + "com.example.local@1.0.0") + if err := os.MkdirAll(staleCacheRoot, 0o755); err != nil { + t.Fatalf("failed to create stale cache package: %v", err) + } + writeManifest( + t, + projectRoot, + `{"dependencies":{"com.example.local":"`+dependencyPrefix+ + filepath.ToSlash(localPackageRoot)+`"}}`) + + results := EnumeratePackageSearchResults(projectRoot) + + actual := packageResultSummaries(results) + expected := []string{"com.example.local|" + filepath.Clean(localPackageRoot)} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("package results mismatch:\nactual: %#v\nexpected: %#v", actual, expected) + } + }) + } +} + // Tests that package search results expose stable package identities for direct, manifest, and cached roots. func TestEnumeratePackageSearchResultsCapturesPackageIdentities(t *testing.T) { projectRoot := t.TempDir() diff --git a/cli/dispatcher/shared-inputs-stamp.json b/cli/dispatcher/shared-inputs-stamp.json index 78cf99d77a..6169431297 100644 --- a/cli/dispatcher/shared-inputs-stamp.json +++ b/cli/dispatcher/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "d2612511f3e961536ba7a96427b4aff4a6f6a2f3" + "sharedInputsHash": "c1b345cfb13b50c52c529f4746a4b481ddbd0615" } diff --git a/cli/project-runner/shared-inputs-stamp.json b/cli/project-runner/shared-inputs-stamp.json index 3202a84f61..26a3229fcc 100644 --- a/cli/project-runner/shared-inputs-stamp.json +++ b/cli/project-runner/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "d61682d89b48e4595a01f18e08c3b96ed648e12a" + "sharedInputsHash": "beb7eb4a81fe05748dd1c11442357fbc06188c5d" }