From 8d24ff2d3306840201e7d3e3f7fb78a8bbb5ea95 Mon Sep 17 00:00:00 2001 From: ModeIO Local Date: Tue, 19 May 2026 14:29:32 +0800 Subject: [PATCH] Update Cursor skills managed root --- skill_manager/application/skills/mutations.py | 7 ++- skill_manager/harness/catalog.py | 3 -- skill_manager/harness/contracts.py | 5 --- tests/integration/test_skills_mutations.py | 44 ++++++++++++++----- tests/support/fake_home.py | 4 ++ tests/unit/test_skills_adapters.py | 34 ++++++++++++++ 6 files changed, 75 insertions(+), 22 deletions(-) diff --git a/skill_manager/application/skills/mutations.py b/skill_manager/application/skills/mutations.py index 4aa7061..8414b9f 100644 --- a/skill_manager/application/skills/mutations.py +++ b/skill_manager/application/skills/mutations.py @@ -66,10 +66,9 @@ def set_skill_all_harnesses(self, skill_ref: str, target: str) -> dict[str, obje failures: list[dict[str, str]] = [] flipped_any = False - # Bulk set-all only targets harnesses whose CLI is installed on the - # system. Enabling on an uninstalled harness would write a symlink - # into a folder no runtime reads, which is misleading and happens - # to cascade across overlapping discovery roots in the catalog. + # Bulk set-all only targets harnesses that are installed or otherwise + # interactable. Enabling on an unavailable harness would write a + # symlink into a folder no runtime reads, which is misleading. for adapter in self.read_models.enabled_installed_adapters(): has_binding = adapter.has_binding(entry.package_dir) if target == "enabled" and has_binding: diff --git a/skill_manager/harness/catalog.py b/skill_manager/harness/catalog.py index df33f22..a546e29 100644 --- a/skill_manager/harness/catalog.py +++ b/skill_manager/harness/catalog.py @@ -112,9 +112,6 @@ def harness_definitions_for_family(family: FamilyKey) -> tuple[HarnessDefinition "skills": FileTreeBindingProfile( managed_env="SKILL_MANAGER_CURSOR_ROOT", managed_default=lambda context: context.home / ".cursor" / "skills", - managed_candidates=( - lambda context: context.home / ".cursor" / "skills-cursor", - ), availability="cli_or_app", app_probe_paths=( lambda _context: Path("/Applications/Cursor.app"), diff --git a/skill_manager/harness/contracts.py b/skill_manager/harness/contracts.py index dcd3421..e9b7c46 100644 --- a/skill_manager/harness/contracts.py +++ b/skill_manager/harness/contracts.py @@ -29,7 +29,6 @@ class FileTreeBindingProfile: shape: Literal["file-tree"] = "file-tree" managed_env: str | None = None managed_default: PathResolver | None = None - managed_candidates: tuple[PathResolver, ...] = () discovery_roots: tuple[FileTreeDiscoveryRoot, ...] = () availability: FileTreeAvailability = "cli" app_probe_paths: tuple[PathResolver, ...] = () @@ -41,10 +40,6 @@ def resolve_managed_root(self, context: ResolutionContext) -> Path: override = context.env.get(self.managed_env) if override: return Path(override) - for resolver in self.managed_candidates: - candidate = resolver(context) - if candidate.is_dir(): - return candidate return self.managed_default(context) diff --git a/tests/integration/test_skills_mutations.py b/tests/integration/test_skills_mutations.py index 89bc59b..460e383 100644 --- a/tests/integration/test_skills_mutations.py +++ b/tests/integration/test_skills_mutations.py @@ -63,6 +63,10 @@ def seed_unmanage_fixture(spec): path.symlink_to(target) +def seed_cursor_owned_skill_fixture(spec): + seed_skill_package(spec.cursor_owned_root, "cursor-built", "Cursor Built") + + class SkillsMutationTests(unittest.TestCase): def test_enable_managed_skill_creates_symlink(self) -> None: with AppTestHarness(fixture_factory=seed_shared_only_fixture) as harness: @@ -149,16 +153,16 @@ def test_set_skill_harnesses_rejects_invalid_target(self) -> None: expected_status=422, ) - def test_set_skill_harnesses_only_targets_installed_harnesses(self) -> None: + def test_set_skill_harnesses_only_targets_available_harnesses(self) -> None: """Bulk set-all must not write symlinks into folders no runtime reads. With only codex + claude CLIs available on PATH, enabling-all should - produce symlinks in those two managed roots only, regardless of how - many harnesses are supported in the catalog. + produce symlinks in those two managed roots plus any valid app-probed + harnesses, regardless of how many harnesses are supported in the catalog. """ with AppTestHarness(fixture_factory=seed_shared_only_fixture) as harness: - # Simulate a machine that only has codex + claude installed by - # removing the other CLI stubs from the fake PATH. + # Simulate missing non-core CLIs by removing their stubs from the + # fake PATH. Cursor may still be available through its app probe. for cli in ("cursor-agent", "opencode", "openclaw"): stub = harness.spec.bin_dir / cli if stub.exists(): @@ -173,7 +177,6 @@ def test_set_skill_harnesses_only_targets_installed_harnesses(self) -> None: installed_by_harness = {col["harness"]: col["installed"] for col in skills["harnessColumns"]} self.assertTrue(installed_by_harness["codex"]) self.assertTrue(installed_by_harness["claude"]) - self.assertFalse(installed_by_harness["cursor"]) self.assertFalse(installed_by_harness["opencode"]) self.assertFalse(installed_by_harness["openclaw"]) @@ -184,12 +187,18 @@ def test_set_skill_harnesses_only_targets_installed_harnesses(self) -> None: self.assertTrue(result["ok"]) self.assertEqual(result["failed"], []) - # Only installed harnesses should flip. - self.assertEqual(set(result["succeeded"]), {"codex", "claude"}) + # Only installed or otherwise interactable harnesses should flip. + expected = {"codex", "claude"} + if installed_by_harness["cursor"]: + expected.add("cursor") + self.assertEqual(set(result["succeeded"]), expected) self.assertTrue((harness.spec.codex_root / "shared-audit").is_symlink()) self.assertTrue((harness.spec.claude_root / "shared-audit").is_symlink()) - # Uninstalled harness folders remain untouched. - self.assertFalse((harness.spec.cursor_root / "shared-audit").exists()) + if installed_by_harness["cursor"]: + self.assertTrue((harness.spec.cursor_root / "shared-audit").is_symlink()) + else: + self.assertFalse((harness.spec.cursor_root / "shared-audit").exists()) + # Unavailable harness folders remain untouched. self.assertFalse((harness.spec.opencode_root / "shared-audit").exists()) self.assertFalse((harness.spec.openclaw_managed_root / "shared-audit").exists()) @@ -220,6 +229,21 @@ def test_manage_all_skills_centralizes_all_found_local_rows(self) -> None: self.assertEqual(result["failures"], []) self.assertEqual(refreshed["summary"]["unmanaged"], 0) + def test_manage_all_ignores_cursor_owned_skills_cursor_directory(self) -> None: + with AppTestHarness(fixture_factory=seed_cursor_owned_skill_fixture) as harness: + skills = harness.get_json("/api/skills") + + self.assertNotIn("Cursor Built", [row["name"] for row in skills["rows"]]) + + result = harness.post_json("/api/skills/manage-all") + + self.assertTrue(result["ok"]) + self.assertEqual(result["managedCount"], 0) + self.assertEqual(result["failures"], []) + self.assertTrue((harness.spec.cursor_owned_root / "cursor-built" / "SKILL.md").is_file()) + self.assertFalse((harness.spec.cursor_owned_root / "cursor-built").is_symlink()) + self.assertFalse((harness.spec.cursor_root / "cursor-built").exists()) + def test_manage_rejects_missing_harness_install_before_creating_bindings(self) -> None: with AppTestHarness(mixed=True) as harness: (harness.spec.bin_dir / "codex").unlink() diff --git a/tests/support/fake_home.py b/tests/support/fake_home.py index 6bf70b6..1b155ae 100644 --- a/tests/support/fake_home.py +++ b/tests/support/fake_home.py @@ -39,6 +39,10 @@ def claude_root(self) -> Path: def cursor_root(self) -> Path: return self.home / ".cursor" / "skills" + @property + def cursor_owned_root(self) -> Path: + return self.home / ".cursor" / "skills-cursor" + @property def opencode_root(self) -> Path: return self.xdg_config_home / "opencode" / "skills" diff --git a/tests/unit/test_skills_adapters.py b/tests/unit/test_skills_adapters.py index 0c49005..0db902d 100644 --- a/tests/unit/test_skills_adapters.py +++ b/tests/unit/test_skills_adapters.py @@ -53,6 +53,40 @@ def test_adapter_reports_missing_cli_as_not_installed(self) -> None: self.assertFalse(openclaw.status().installed) self.assertEqual(openclaw.scan().skills, ()) + def test_cursor_skills_use_skills_root_and_ignore_skills_cursor(self) -> None: + with TemporaryDirectory() as temp_dir: + spec = create_fake_home_spec(Path(temp_dir)) + seed_skill_package(spec.cursor_root, "managed-cursor", "Managed Cursor") + seed_skill_package(spec.cursor_owned_root, "cursor-built", "Cursor Built") + + cursor = _adapter("cursor", spec) + scan = cursor.scan() + + self.assertEqual(cursor.managed_root, spec.cursor_root) + self.assertEqual( + [skill.package.declared_name for skill in scan.skills], + ["Managed Cursor"], + ) + + def test_cursor_app_probe_keeps_skills_adapter_installed_without_cli(self) -> None: + with TemporaryDirectory() as temp_dir: + spec = create_fake_home_spec(Path(temp_dir)) + (spec.bin_dir / "cursor-agent").unlink() + (spec.home / "Applications" / "Cursor.app").mkdir(parents=True) + + cursor = _adapter("cursor", spec) + kernel = HarnessKernelService.from_environment( + spec.env(), + support_store=HarnessSupportStore(spec.root / "settings.json"), + ) + cursor_status = next( + status for status in kernel.harness_statuses() if status.harness == "cursor" + ) + + self.assertTrue(cursor.status().installed) + self.assertTrue(cursor_status.installed) + self.assertEqual(cursor_status.managed_location, spec.cursor_root) + def test_enable_creates_symlink(self) -> None: with TemporaryDirectory() as temp_dir: spec = create_fake_home_spec(Path(temp_dir))