diff --git a/hermes_cli/plugins_cmd_install.py b/hermes_cli/plugins_cmd_install.py index b7fd9556d0..3f06aced8f 100644 --- a/hermes_cli/plugins_cmd_install.py +++ b/hermes_cli/plugins_cmd_install.py @@ -385,6 +385,15 @@ def _install_plugin_core( manifest = _read_manifest_for_install(tmp_target) plugin_name = manifest.get("name") or ( subdir.rstrip("/").rsplit("/", 1)[-1] if subdir else _pc()._repo_name_from_url(git_url)) + link = plugins_dir / str(plugin_name) + from pm.filesystem import is_junction + if "/" not in str(plugin_name) and (link.is_symlink() or is_junction(link)): + # A provider's own installer (e.g. `mnemosyne-hermes install`) links its package here; the + # name is fine, the slot is taken. Say so instead of blaming the manifest. + raise _pc().PluginOperationError( + f"Plugin '{plugin_name}' is already installed outside the catalog: {link} is a link to " + f"{os.path.realpath(link)}. Delete that link to install the catalog version.", + failure_class="already_installed") try: target = _pc()._sanitize_plugin_name(plugin_name, plugins_dir) except ValueError as e: diff --git a/tests/hermes_cli/test_plugin_install_manifest_version.py b/tests/hermes_cli/test_plugin_install_manifest_version.py index 4234f40b5b..460247c9f1 100644 --- a/tests/hermes_cli/test_plugin_install_manifest_version.py +++ b/tests/hermes_cli/test_plugin_install_manifest_version.py @@ -85,3 +85,21 @@ def test_manifest_version_above_shared_support_is_refused_cleanly( assert not (home / "plugins" / "demo").exists() assert not (home / "plugins" / ".install-metadata.json").exists() + + +def test_a_linked_plugin_slot_is_already_installed_not_a_bad_manifest(monkeypatch, tmp_path): + """A provider's own installer can leave ``plugins/`` as a link into a venv (mnemosyne-hermes + did). The catalog install must say the slot is taken, not record ``manifest_invalid``.""" + from hermes_cli import plugins_cmd + + monkeypatch.setattr(plugins_cmd, "_scan_on_install_enabled", lambda: False) + repo = _plugin_repo(tmp_path, {"name": "demo", "version": "1.0.0"}) + home = tmp_path / "home" + monkeypatch.setenv("HERMES_HOME", str(home)) + (home / "plugins").mkdir(parents=True) + (tmp_path / "elsewhere").mkdir() + (home / "plugins" / "demo").symlink_to(tmp_path / "elsewhere", target_is_directory=True) + + with pytest.raises(plugins_cmd.PluginOperationError, match="already installed outside the catalog") as exc: + plugins_cmd._install_plugin_core(repo.as_uri(), force=False) + assert exc.value.failure_class == "already_installed"