From ffa105c7ae9fe6000e395e274c6d36dd2b80436a Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:14:45 -0700 Subject: [PATCH] fix(memory): migration checks provider presence in the home being migrated --- hermes_cli/memory_provider_migration.py | 15 +++++++++++---- .../test_memory_provider_migration.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/hermes_cli/memory_provider_migration.py b/hermes_cli/memory_provider_migration.py index 808cfa4e87..8e6205e7c5 100644 --- a/hermes_cli/memory_provider_migration.py +++ b/hermes_cli/memory_provider_migration.py @@ -32,10 +32,17 @@ def configured_provider(home: Path) -> str: return str(memory.get("provider") or "").strip() -def provider_present(name: str) -> bool: - """True when the provider resolves anywhere Hermes looks (bundled, user dir, entry point).""" +def provider_present(name: str, home: Path) -> bool: + """True when the provider resolves anywhere Hermes looks for *home* (bundled, that home's user + plugins, entry point). The lookup reads the active home, so it is bound explicitly: the update + hook walks several profile homes from one process.""" from plugins.memory import find_provider_dir - return find_provider_dir(name) is not None + from hermes_constants import reset_hermes_home_override, set_hermes_home_override + token = set_hermes_home_override(home) + try: + return find_provider_dir(name) is not None + finally: + reset_hermes_home_override(token) def catalog_source(name: str) -> Optional[str]: @@ -53,7 +60,7 @@ def migrate_home(home: Path, *, install: Callable[[str], dict], say: Callable[[s update or the agent down with it. """ name = configured_provider(home) - if not name or provider_present(name): + if not name or provider_present(name, home): return None if catalog_source(name) is None: say(f" ⚠ Memory provider '{name}' is configured but not installed and not in the plugin catalog. " diff --git a/tests/hermes_cli/test_memory_provider_migration.py b/tests/hermes_cli/test_memory_provider_migration.py index 04f51c35e7..a2fbc6dec3 100644 --- a/tests/hermes_cli/test_memory_provider_migration.py +++ b/tests/hermes_cli/test_memory_provider_migration.py @@ -12,7 +12,7 @@ from hermes_cli import memory_provider_migration as mig def home(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) (tmp_path / "config.yaml").write_text("memory:\n provider: honcho\n honcho:\n workspace: keep-me\n") - monkeypatch.setattr(mig, "provider_present", lambda name: (tmp_path / "plugins" / name).is_dir()) + monkeypatch.setattr(mig, "provider_present", lambda name, home: (home / "plugins" / name).is_dir()) return tmp_path @@ -35,6 +35,22 @@ def test_missing_provider_installs_its_catalog_plugin_and_keeps_config(home, mon assert calls == ["honcho"] +def test_presence_is_checked_in_the_home_being_migrated(tmp_path, monkeypatch): + """The update hook walks several profile homes from one process; a provider installed in profile B + must count as present for B even when the process-level home (A) lacks it. Real lookup, no mock.""" + a, b = tmp_path / "a", tmp_path / "b" + for h in (a, b): + h.mkdir(); (h / "config.yaml").write_text("memory:\n provider: twin\n") + (b / "plugins" / "twin").mkdir(parents=True) + (b / "plugins" / "twin" / "__init__.py").write_text("class Twin(MemoryProvider): ...\n") + monkeypatch.setenv("HERMES_HOME", str(a)) + monkeypatch.setattr(mig, "catalog_source", lambda name: name) + installs: list[Path] = [] + assert mig.migrate_home(b, install=lambda n: installs.append(b) or {"ok": True}, say=lambda s: None) is None + assert installs == [] + assert mig.migrate_home(a, install=lambda n: installs.append(a) or {"ok": True}, say=lambda s: None) == "twin" + + def test_provider_unknown_to_catalog_is_reported_not_installed(home, monkeypatch): monkeypatch.setattr(mig, "catalog_source", lambda name: None) said: list[str] = []