Files
hermes-agent/tests/test_hermes_home_key_cache.py
Adolanium 4a17802280 perf(constants): remember the resolved Hermes home key
`hermes_home_key` called `Path.resolve()` on every call, which is a
filesystem call. `ToolRegistry.current_scope_key()` calls it, and
`_merged_tools()` calls that, so every single registry lookup went to
the disk: `get_entry`, `get_toolset_for_tool`, `get_emoji`, and the rest.

Anything that looks up many tools in a row paid for it. Building the
deferred-tool catalog for `tool_search` calls `get_entry` once per tool,
so with 400 deferrable tools one search did 800 path resolutions. On
Windows that is `nt._getfinalpathname`, and it took 132ms of the 148ms
the catalog build spent. The tokenizing everyone would suspect first was
under 4ms of it.

The resolved value is now remembered per input path. A process only sees
a handful of home paths, so the dict stays tiny. Measured on the same
400-tool catalog build, the lookups drop from 132ms to 3.1ms with one
entry cached.

A path that does not exist yet is resolved but not remembered, because
the answer can change once it is created, for instance when part of the
path turns out to be a link. A different home path is a different key,
so switching profiles or `HERMES_HOME` gets its own entry rather than
reusing a stale one.
2026-09-03 02:06:56 +05:30

165 lines
5.8 KiB
Python

"""Tests for the remembered results in `hermes_home_key`.
`Path.resolve()` is a filesystem call. `hermes_home_key` sits under
`ToolRegistry.current_scope_key()`, which runs on every registry lookup, so
before the results were remembered the registry paid a syscall per lookup.
The value this returns must not change, so most of these tests compare
against the plain uncached calculation.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
import hermes_constants as hc
def _uncached(path=None) -> str:
"""The calculation as it was before results were remembered."""
candidate = Path(path) if path is not None else hc.get_hermes_home()
return os.path.normcase(str(candidate.expanduser().resolve(strict=False)))
@pytest.fixture(autouse=True)
def _clear_cache():
hc.reset_hermes_home_key_cache()
yield
hc.reset_hermes_home_key_cache()
class TestSameAnswerAsBefore:
"""Remembering a result must not change what comes back."""
@pytest.mark.parametrize("case", ["real", "missing", "trailing_sep", "dot_dot"])
def test_matches_the_uncached_calculation(self, tmp_path, case):
(tmp_path / "real").mkdir()
target = {
"real": str(tmp_path / "real"),
"missing": str(tmp_path / "not_there"),
"trailing_sep": str(tmp_path / "real") + os.sep,
"dot_dot": str(tmp_path / "real" / ".." / "real"),
}[case]
assert hc.hermes_home_key(target) == _uncached(target)
def test_matches_for_the_default_home(self):
assert hc.hermes_home_key() == _uncached()
def test_matches_for_a_tilde_path(self):
assert hc.hermes_home_key("~") == _uncached("~")
def test_accepts_a_path_object(self, tmp_path):
(tmp_path / "real").mkdir()
assert hc.hermes_home_key(tmp_path / "real") == _uncached(tmp_path / "real")
def test_second_call_returns_the_same_string(self, tmp_path):
(tmp_path / "real").mkdir()
first = hc.hermes_home_key(str(tmp_path / "real"))
second = hc.hermes_home_key(str(tmp_path / "real"))
assert first == second == _uncached(str(tmp_path / "real"))
class TestWhatGetsRemembered:
def test_an_existing_path_is_remembered(self, tmp_path):
(tmp_path / "real").mkdir()
hc.hermes_home_key(str(tmp_path / "real"))
assert len(hc._HOME_KEY_CACHE) == 1
def test_a_missing_path_is_not_remembered(self, tmp_path):
# The answer can change once the directory is created, for example
# when part of the path turns out to be a link, so it must not stick.
hc.hermes_home_key(str(tmp_path / "not_there"))
assert hc._HOME_KEY_CACHE == {}
def test_a_path_created_later_picks_up_the_real_answer(self, tmp_path):
later = tmp_path / "later"
before = hc.hermes_home_key(str(later))
later.mkdir()
after = hc.hermes_home_key(str(later))
assert after == _uncached(str(later))
assert hc._HOME_KEY_CACHE == {str(later): after}
# On a plain directory both answers agree anyway. The point is that
# the first one was never stored.
assert before == after
def test_different_paths_get_their_own_entries(self, tmp_path):
for name in ("a", "b", "c"):
(tmp_path / name).mkdir()
hc.hermes_home_key(str(tmp_path / name))
assert len(hc._HOME_KEY_CACHE) == 3
def test_reset_clears_everything(self, tmp_path):
(tmp_path / "real").mkdir()
hc.hermes_home_key(str(tmp_path / "real"))
assert hc._HOME_KEY_CACHE
hc.reset_hermes_home_key_cache()
assert hc._HOME_KEY_CACHE == {}
class TestHomeChanges:
def test_pointing_hermes_home_somewhere_else_gives_a_new_key(
self, tmp_path, monkeypatch,
):
# A different home is a different input path, so it lands on its own
# entry rather than reusing the first one.
first = tmp_path / "home_one"
second = tmp_path / "home_two"
first.mkdir()
second.mkdir()
monkeypatch.setenv("HERMES_HOME", str(first))
key_one = hc.hermes_home_key()
monkeypatch.setenv("HERMES_HOME", str(second))
key_two = hc.hermes_home_key()
assert key_one != key_two
assert key_one == _uncached(str(first))
assert key_two == _uncached(str(second))
class TestSymlinks:
def test_a_link_resolves_to_its_target(self, tmp_path):
target = tmp_path / "target"
target.mkdir()
link = tmp_path / "link"
try:
link.symlink_to(target, target_is_directory=True)
except (OSError, NotImplementedError):
pytest.skip("this platform or account cannot create symlinks")
assert hc.hermes_home_key(str(link)) == _uncached(str(link))
assert hc.hermes_home_key(str(link)) == hc.hermes_home_key(str(target))
class TestRegistryLookupsDoNotHitTheDisk:
def test_scope_key_resolves_the_path_once(self, monkeypatch):
# The reason this cache exists. ToolRegistry.current_scope_key() runs
# on every registry lookup, so it must not resolve the home path on
# the filesystem every time.
from tools.registry import registry
calls = {"n": 0}
real_resolve = Path.resolve
def counting_resolve(self, *a, **kw):
calls["n"] += 1
return real_resolve(self, *a, **kw)
monkeypatch.setattr(Path, "resolve", counting_resolve)
registry.current_scope_key()
after_first = calls["n"]
for _ in range(50):
registry.current_scope_key()
assert calls["n"] == after_first, (
f"current_scope_key resolved the path on the filesystem "
f"{calls['n'] - after_first} extra times across 50 calls"
)
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))