Files
hermes-agent/tests/plugins/memory/test_hindsight_embedded_runtime.py
ethernet a9845b8a04 fix(hindsight): serve the routed profile's env to the daemon; probe once per interpreter
_daemon_subprocess_env copied dict(os.environ), which under multiplex is
the LAUNCH profile's: profile X's daemon started with the default
profile's HERMES_HOME and credentials. It now builds the child env with
served_profile_child_env(inherit_credentials=False) — the manager reads
the LLM keys from the profile's own 0600 env file, so the child needs no
credentials from us.

check_local_runtime spawned 'python -c import ... sentence_transformers'
(a torch cold start) from is_available(), unavailable_reason() and
initialize() on every session. The verdict is now kept per interpreter
path for the process; a reinstall publishes a new PM generation, so a
new path re-probes.
2026-09-21 18:57:44 -04:00

224 lines
9.7 KiB
Python

"""Hindsight's shipped probe and bridge execute in an isolated side interpreter.
The tiny SDK models only the child protocol, not upstream ML/CPU compatibility.
PM generation acquisition is owned by tests/pm; no upstream downloads occur here.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
import plugins.memory.hindsight.embedded_runtime as rt
@pytest.fixture()
def side_root(tmp_path, monkeypatch):
root = tmp_path / "hermes-home" / "profiles" / "Hindsight" / "env"
monkeypatch.setattr(rt, "sideenv_root", lambda: root)
monkeypatch.setattr(rt, "_probe_verdicts", {})
return root
def test_ensure_sideenv_uses_pm_selected_python(side_root, monkeypatch):
import pm
from pm.environments import venv_python
python = venv_python(side_root / "selected")
seen = {}
def ensure(name, requirements, **kwargs):
seen.update(name=name, requirements=requirements, **kwargs)
return python
monkeypatch.setattr(pm, "ensure_environment", ensure)
assert rt.ensure_sideenv() == python
assert seen == dict(
name="hindsight", requirements=(f"hindsight-embed=={rt._EMBED_VERSION}",
f"hindsight-api-slim[all]=={rt._API_SLIM_VERSION}"),
root=side_root, explicit=True, timeout=3600,
)
def test_sideenv_python_is_pm_passive_selection(side_root, monkeypatch):
import pm
from pm.environments import venv_python
python = venv_python(side_root / "selected")
seen = []
monkeypatch.setattr(pm, "environment_python",
lambda name, **kw: seen.append((name, kw)) or python)
assert rt.sideenv_python() == python
assert seen == [("hindsight", {"root": side_root})]
assert not side_root.exists()
@pytest.fixture
def side_python(side_root, monkeypatch, tmp_path):
import pm
subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(side_root)],
check=True, capture_output=True, timeout=30)
from pm.environments import venv_python
python = venv_python(side_root)
site = Path(subprocess.check_output(
[str(python), "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"],
text=True, timeout=10,
).strip())
sdk = site / "hindsight_embed"
sdk.mkdir()
(sdk / "__init__.py").touch()
for module in (sdk / "daemon_embed_manager.py", site / "hindsight_api.py", site / "sentence_transformers.py"):
module.write_text("", encoding="utf-8")
(sdk / "daemon_client.py").write_text("""import json, os, sys
from pathlib import Path
stopped = None
def stop_daemon(profile):
global stopped
stopped = profile
def ensure_daemon_running(config, profile):
Path(os.environ['TEST_RECEIPT']).write_text(json.dumps({
'config': config, 'profile': profile, 'stopped': stopped,
'prefix': sys.prefix, 'env': dict(os.environ)}), encoding='utf-8')
print('manager startup noise')
return True
def get_daemon_url(profile):
return 'http://127.0.0.1:54321/' + profile
""", encoding="utf-8")
receipt = tmp_path / "bridge.json"
monkeypatch.setenv("TEST_RECEIPT", str(receipt))
monkeypatch.setattr(pm, "environment_python", lambda *a, **kw: python)
return site, receipt
@pytest.mark.parametrize("grace,override,expected", [
(60, None, "60.0"), ("45", None, "45.0"), (0, None, "0.0"),
(None, None, None), ("", None, None), ("invalid", None, None),
(-5, None, None), (60, "99", "99"),
])
def test_side_interpreter_probe_and_bridge(side_python, side_root, monkeypatch, caplog, grace, override, expected):
site, receipt = side_python
markers = ("VIRTUAL_ENV", "PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP", "PYTHONEXECUTABLE")
for key in markers:
monkeypatch.setenv(key, "/invalid/main-environment")
monkeypatch.delenv("HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT", raising=False)
if override:
monkeypatch.setenv("HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT", override)
parent = dict(os.environ)
assert rt.check_local_runtime() == (True, None)
with caplog.at_level("INFO", logger=rt.__name__):
assert rt.ensure_daemon_and_url({"profile": "test-profile", "port_health_grace_timeout": grace}, restart=True) == "http://127.0.0.1:54321/test-profile"
child = json.loads(receipt.read_text(encoding="utf-8"))
assert child["config"] == {}
assert child["profile"] == child["stopped"] == "test-profile"
assert Path(child["prefix"]) == side_root
assert child["env"].get("HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT") == expected
assert child["env"]["HINDSIGHT_EMBED_API_VERSION"] == rt._API_SLIM_VERSION
assert not set(markers).intersection(child["env"])
assert dict(os.environ) == parent
assert not {"hindsight_embed", "hindsight_api", "sentence_transformers"}.intersection(sys.modules)
assert "daemon ready" in caplog.text and "starting side-env daemon manager" in caplog.text
(site / "sentence_transformers.py").write_text("raise RuntimeError('NumPy SIMD unavailable')", encoding="utf-8")
rt._probe_verdicts.clear() # same interpreter path mutated in place; a real reinstall is a new generation
ok, reason = rt.check_local_runtime()
assert not ok and "NumPy SIMD unavailable" in reason
def test_probe_verdict_is_reused_within_the_process_until_the_interpreter_changes(side_root, monkeypatch):
"""is_available()/unavailable_reason()/initialize() each ask per session; the probe is a
torch cold start. One spawn per interpreter path, re-probed when PM publishes a new one."""
from pm.environments import venv_python
interpreters = [venv_python(side_root / "gen-1"), venv_python(side_root / "gen-2")]
monkeypatch.setattr(rt, "sideenv_python", lambda root=None: interpreters[0])
spawned = []
def run(argv, *a, **k):
spawned.append(argv[0])
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(rt, "subprocess", SimpleNamespace(run=run, TimeoutExpired=subprocess.TimeoutExpired))
for _ in range(3):
assert rt.check_local_runtime() == (True, None)
assert spawned == [str(interpreters[0])]
interpreters.reverse()
assert rt.check_local_runtime() == (True, None)
assert spawned == [str(interpreters[1]), str(interpreters[0])]
def test_daemon_env_is_the_served_profiles_not_the_launch_environ(tmp_path, monkeypatch):
"""Under multiplex os.environ is the LAUNCH profile's. A daemon started for a routed profile
must see that profile's HERMES_HOME and none of the launch profile's credentials."""
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
launch = tmp_path / "launch"
routed = launch / "profiles" / "beta"
routed.mkdir(parents=True)
(launch / ".env").write_text("OPENAI_API_KEY=sk-launch\nHINDSIGHT_API_KEY=hs-launch\n", encoding="utf-8")
monkeypatch.setenv("HERMES_HOME", str(launch))
monkeypatch.setenv("OPENAI_API_KEY", "sk-launch")
monkeypatch.setenv("HINDSIGHT_API_KEY", "hs-launch")
monkeypatch.setenv("VIRTUAL_ENV", "/invalid/main-environment")
token = set_hermes_home_override(routed)
try:
env = rt._daemon_subprocess_env({"port_health_grace_timeout": 30})
finally:
reset_hermes_home_override(token)
assert Path(env["HERMES_HOME"]) == routed
assert "OPENAI_API_KEY" not in env and "HINDSIGHT_API_KEY" not in env
assert "VIRTUAL_ENV" not in env
assert env["HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT"] == "30.0"
assert env["HINDSIGHT_EMBED_API_VERSION"] == rt._API_SLIM_VERSION
@pytest.mark.parametrize("outcome,message", [
(SimpleNamespace(returncode=1, stdout="", stderr="illegal instruction"), "illegal instruction"),
(SimpleNamespace(returncode=0, stdout='__HERMES_HINDSIGHT_BRIDGE__{"ok": false, "error": "daemon refused"}', stderr=""), "daemon refused"),
(SimpleNamespace(returncode=0, stdout="noise only", stderr=""), "no bridge result"),
(subprocess.TimeoutExpired("side-python", 5), "timed out"),
(OSError("cannot execute"), "could not run"),
])
def test_bridge_failure_is_actionable(side_root, monkeypatch, caplog, outcome, message):
monkeypatch.setattr(rt, "sideenv_python", lambda: side_root / "python")
def run(*args, **kwargs):
if isinstance(outcome, Exception):
raise outcome
return outcome
monkeypatch.setattr(rt, "subprocess", SimpleNamespace(run=run, TimeoutExpired=subprocess.TimeoutExpired))
with pytest.raises(RuntimeError, match=message):
rt.ensure_daemon_and_url({})
if not isinstance(outcome, OSError):
assert caplog.records
@pytest.mark.parametrize("mode,available", [("cloud", False), ("local_external", False), ("local_embedded", True), ("local_embedded", False)])
def test_provider_unavailable_reason(side_root, monkeypatch, mode, available):
import plugins.memory.hindsight as hs
monkeypatch.setattr(hs, "_load_config", lambda: {"mode": mode})
def probe():
assert mode == "local_embedded"
return available, "NumPy SIMD unavailable"
monkeypatch.setattr(hs, "_check_local_runtime", probe)
reason = hs.HindsightMemoryProvider().unavailable_reason()
if mode != "local_embedded" or available:
assert reason == ""
else:
assert reason == reason.strip()
assert "NumPy SIMD unavailable" in reason and "hermes memory setup" in reason
assert str(side_root) in reason
assert all(requirement in reason for requirement in rt._REQUIREMENTS)
assert "uv pip install --python" not in reason
def test_missing_side_environment_never_starts_daemon(side_root):
ok, reason = rt.check_local_runtime()
assert not ok and "hermes memory setup" in reason and str(side_root) in reason
with pytest.raises(RuntimeError, match="hermes memory setup"):
rt.ensure_daemon_and_url({})