diff --git a/plugins/memory/holographic/__init__.py b/plugins/memory/holographic/__init__.py index aede5a1323..63f6786735 100644 --- a/plugins/memory/holographic/__init__.py +++ b/plugins/memory/holographic/__init__.py @@ -21,6 +21,10 @@ from hermes_cli.config import cfg_get logger = logging.getLogger(__name__) +# Expanded by initialize() against the profile that opens it. A concrete path is copied by a profile clone +# and outlives a rename, so it keeps naming the old profile's DB. +_DEFAULT_DB_PATH = "$HERMES_HOME/memory_store.db" + FACT_STORE_SCHEMA = { "name": "fact_store", @@ -116,12 +120,17 @@ class HolographicMemoryProvider(MemoryProvider): # The canonical writer: config lock, managed-mode refusal, default stripping, atomic replace. # ``merge_existing`` keeps every other section; *hermes_home* is the active profile already. from hermes_cli.config import save_config - save_config({"plugins": {"hermes-memory-store": dict(values)}}, merge_existing=True) + values = dict(values) + # This profile's own DB spelled out (older setups wrote it; the dashboard form re-submits what it + # read) pins every clone and rename of the profile to this file, so it is stored as the placeholder. + db_path = values.get("db_path") + if isinstance(db_path, str) and Path(db_path).expanduser() == Path(hermes_home) / "memory_store.db": + values["db_path"] = _DEFAULT_DB_PATH + save_config({"plugins": {"hermes-memory-store": values}}, merge_existing=True) def get_config_schema(self): - from hermes_constants import display_hermes_home return [ - {"key": "db_path", "description": "SQLite database path", "default": f"{display_hermes_home()}/memory_store.db"}, + {"key": "db_path", "description": "SQLite database path", "default": _DEFAULT_DB_PATH}, {"key": "auto_extract", "description": "Auto-extract facts at session end", "default": "false", "choices": ["true", "false"]}, {"key": "default_trust", "description": "Default trust score for new facts", "default": "0.5"}, {"key": "hrr_dim", "description": "HRR vector dimensions", "default": "1024"}, diff --git a/tests/plugins/memory/test_holographic_profile_db_path.py b/tests/plugins/memory/test_holographic_profile_db_path.py new file mode 100644 index 0000000000..7a73304bcd --- /dev/null +++ b/tests/plugins/memory/test_holographic_profile_db_path.py @@ -0,0 +1,96 @@ +"""The holographic fact DB set up with the default path belongs to whichever profile opens it. + +Setup used to offer, and store, the active profile's concrete path (``~/.hermes/memory_store.db``). +``profile create --clone`` copies config.yaml verbatim, so the clone opened the source profile's +facts; ``profile rename`` moves the directory, so the renamed profile opened an empty DB in a +re-created directory under its old name. +""" +from __future__ import annotations + +import io +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest +import yaml + +import hermes_cli.memory_setup as memory_setup +from hermes_cli.profiles import create_profile, rename_profile +from plugins.memory.holographic import HolographicMemoryProvider, _load_plugin_config + + +@pytest.fixture() +def root(tmp_path, monkeypatch): + # A ``~/`` path is displayed through Path.home() and expanded through HOME; both name the sandbox. + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setattr(Path, "home", lambda: tmp_path) + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + return home + + +def _setup_with_defaults(monkeypatch, home: Path) -> dict: + """``hermes memory setup`` for holographic with Enter on every field, saved as ``cmd_setup`` does.""" + monkeypatch.setenv("HERMES_HOME", str(home)) + provider = HolographicMemoryProvider(config={"hrr_dim": 64}) + monkeypatch.setattr(memory_setup, "_curses_select", lambda *_a, default=0, **_k: default) + monkeypatch.setattr(sys, "stdin", io.StringIO("\n" * 10)) + answers: dict = {} + assert memory_setup._prompt_schema_fields("holographic", provider.get_config_schema(), answers, {}) + provider.save_config(answers, str(home)) + return answers + + +def _opened_db(monkeypatch, home: Path) -> Path: + """The DB a session started with ``hermes -p `` opens.""" + monkeypatch.setenv("HERMES_HOME", str(home)) + provider = HolographicMemoryProvider(config=_load_plugin_config()) + provider.initialize("s") + try: + return provider._store.db_path.resolve() + finally: + provider.shutdown() + + +@pytest.mark.parametrize("lifecycle", ["clone", "rename"]) +def test_default_db_path_follows_the_profile_that_opens_it(root, monkeypatch, lifecycle): + if lifecycle == "clone": + answers = _setup_with_defaults(monkeypatch, root) + monkeypatch.setenv("HERMES_HOME", str(root)) + profile = create_profile("work", clone_config=True, no_alias=True) + else: + source = create_profile("alpha", no_alias=True) + answers = _setup_with_defaults(monkeypatch, source) + monkeypatch.setenv("HERMES_HOME", str(root)) + with patch("hermes_cli.profiles.check_alias_collision", return_value="skip"): + profile = rename_profile("alpha", "beta") + + assert _opened_db(monkeypatch, profile) == (profile / "memory_store.db").resolve() + if lifecycle == "rename": + assert not (root / "profiles" / "alpha").exists() + # The prompt offers the value it stores, so what the user accepted is what config.yaml says. + assert answers["db_path"] == "$HERMES_HOME/memory_store.db" + + +@pytest.mark.parametrize("spelling", ["display", "absolute"]) +def test_save_config_stores_this_profiles_default_path_as_the_placeholder(root, monkeypatch, spelling): + # The concrete value an older setup wrote, and the dashboard form re-submits on its next save. + profile = create_profile("work", no_alias=True) + monkeypatch.setenv("HERMES_HOME", str(profile)) + concrete = "~/.hermes/profiles/work/memory_store.db" if spelling == "display" else str(profile / "memory_store.db") + + HolographicMemoryProvider(config={"hrr_dim": 64}).save_config({"db_path": concrete, "hrr_dim": "64"}, str(profile)) + + stored = yaml.safe_load((profile / "config.yaml").read_text(encoding="utf-8"))["plugins"]["hermes-memory-store"] + assert stored == {"db_path": "$HERMES_HOME/memory_store.db", "hrr_dim": "64"} + + +def test_save_config_keeps_an_explicit_path(root, monkeypatch): + elsewhere = str(root / "shared" / "facts.db") + + HolographicMemoryProvider(config={"hrr_dim": 64}).save_config({"db_path": elsewhere}, str(root)) + + stored = yaml.safe_load((root / "config.yaml").read_text(encoding="utf-8"))["plugins"]["hermes-memory-store"] + assert stored["db_path"] == elsewhere