mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-31 06:51:29 +00:00
Remove unused imports (F401) and duplicate/shadowed import redefinitions (F811) across the codebase using ruff's safe autofixes. No behavioral changes -- imports only. - ~1400 safe autofixes applied across 644 files (net -1072 lines) - __init__.py re-exports preserved (excluded from F401 removal so public re-export surfaces stay intact) - Re-exports that are imported or monkeypatched by tests but look unused in their defining module are kept with explicit # noqa: F401 (gateway/run.py load_dotenv; run_agent re-exports from agent.message_sanitization, agent.context_compressor, agent.retry_utils, agent.prompt_builder, agent.process_bootstrap, agent.codex_responses_adapter) - Unsafe F841 (unused-variable) fixes deliberately skipped -- those can change behavior when the RHS has side effects - ruff lints remain disabled in pyproject.toml (only PLW1514 is selected); this is a one-time cleanup, not a config change Verification: - python -m compileall: clean - pytest --collect-only: all 27161 tests collect (zero import errors) - core entry points import clean (run_agent, model_tools, cli, toolsets, hermes_state, batch_runner, gateway) - static scan: every name any test imports directly from an edited module still resolves
883 lines
34 KiB
Python
883 lines
34 KiB
Python
"""Tests for the ``pinPeerName`` / ``pinUserPeer`` config flag.
|
|
|
|
Under a gateway (Telegram, Discord, Slack, ...) Hermes passes the
|
|
platform-native user ID as ``runtime_user_peer_name`` into
|
|
``HonchoSessionManager``. By default that ID wins over any configured
|
|
``peer_name`` so multi-user bots scope memory per user.
|
|
|
|
For single-user deployments connecting over multiple platforms,
|
|
``pinUserPeer: true`` pins the user peer to ``peer_name`` so memory stays
|
|
unified across platforms.
|
|
|
|
Tests cover config parsing (``client.py::from_global_config``) and resolver
|
|
order (``session.py::get_or_create``), stubbing Honcho API calls so the
|
|
chosen ``user_peer_id`` can be asserted without touching the network.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
from plugins.memory.honcho.client import HonchoClientConfig
|
|
from plugins.memory.honcho.session import HonchoSessionManager
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPinPeerNameConfigParsing:
|
|
def test_default_is_false(self):
|
|
"""Default preserves existing behaviour — multi-user bots unaffected."""
|
|
config = HonchoClientConfig()
|
|
assert config.pin_peer_name is False
|
|
|
|
def test_root_level_true(self, tmp_path, monkeypatch):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"pinPeerName": True,
|
|
}))
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is True
|
|
assert config.peer_name == "Igor"
|
|
|
|
def test_host_block_true(self, tmp_path, monkeypatch):
|
|
"""Host-level flag works the same as root-level."""
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"hosts": {
|
|
"hermes": {"pinPeerName": True},
|
|
},
|
|
}))
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is True
|
|
|
|
def test_host_block_overrides_root(self, tmp_path, monkeypatch):
|
|
"""Host block wins over root — matches how every other flag behaves."""
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"pinPeerName": True,
|
|
"hosts": {
|
|
"hermes": {"pinPeerName": False},
|
|
},
|
|
}))
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is False, (
|
|
"host-level pinPeerName=false must override root-level true, the "
|
|
"same way every other flag in this config is resolved"
|
|
)
|
|
|
|
def test_explicit_false_parses(self, tmp_path, monkeypatch):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"pinPeerName": False,
|
|
}))
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is False
|
|
|
|
|
|
class TestRuntimePeerMappingConfigParsing:
|
|
def test_defaults_are_empty(self):
|
|
config = HonchoClientConfig()
|
|
assert config.user_peer_aliases == {}
|
|
assert config.runtime_peer_prefix == ""
|
|
|
|
def test_root_level_aliases_and_prefix_parse(self, tmp_path):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"userPeerAliases": {
|
|
" 86701400 ": " Igor ",
|
|
"": "ignored",
|
|
"empty-value": " ",
|
|
"null-value": None,
|
|
},
|
|
"runtimePeerPrefix": "telegram_",
|
|
}))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
assert config.user_peer_aliases == {"86701400": "Igor"}
|
|
assert config.runtime_peer_prefix == "telegram_"
|
|
|
|
def test_host_aliases_override_root_aliases_as_whole_map(self, tmp_path):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"userPeerAliases": {"root-user": "root-peer"},
|
|
"hosts": {
|
|
"hermes": {
|
|
"userPeerAliases": {"host-user": "host-peer"},
|
|
},
|
|
},
|
|
}))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
assert config.user_peer_aliases == {"host-user": "host-peer"}
|
|
|
|
def test_host_empty_aliases_disable_root_aliases(self, tmp_path):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"userPeerAliases": {"root-user": "root-peer"},
|
|
"hosts": {
|
|
"hermes": {
|
|
"userPeerAliases": {},
|
|
},
|
|
},
|
|
}))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
assert config.user_peer_aliases == {}
|
|
|
|
def test_host_empty_prefix_disables_root_prefix(self, tmp_path):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"runtimePeerPrefix": "telegram_",
|
|
"hosts": {
|
|
"hermes": {
|
|
"runtimePeerPrefix": "",
|
|
},
|
|
},
|
|
}))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
assert config.runtime_peer_prefix == ""
|
|
|
|
def test_malformed_alias_config_is_ignored(self, tmp_path):
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"userPeerAliases": ["not", "a", "map"],
|
|
}))
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
assert config.user_peer_aliases == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Peer resolution (the actual bug fix)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _patch_manager_for_resolution_test(mgr: HonchoSessionManager) -> None:
|
|
"""Stub out the Honcho client so ``get_or_create`` doesn't try to talk
|
|
to the network — we only care about the user_peer_id chosen before
|
|
those calls happen.
|
|
"""
|
|
fake_peer = MagicMock()
|
|
mgr._get_or_create_peer = MagicMock(return_value=fake_peer)
|
|
mgr._get_or_create_honcho_session = MagicMock(
|
|
return_value=(MagicMock(), [])
|
|
)
|
|
|
|
|
|
class TestPeerResolutionOrder:
|
|
"""Matrix of (runtime_id, pin_peer_name, peer_name) → expected user_peer_id."""
|
|
|
|
def _config(
|
|
self,
|
|
*,
|
|
peer_name: str | None,
|
|
pin_peer_name: bool,
|
|
user_peer_aliases: dict[str, str] | None = None,
|
|
runtime_peer_prefix: str = "",
|
|
session_peer_prefix: bool = False,
|
|
) -> HonchoClientConfig:
|
|
# The test doesn't need auth / Honcho — disable the provider so
|
|
# the manager doesn't try to open a real client.
|
|
return HonchoClientConfig(
|
|
api_key="test-key",
|
|
peer_name=peer_name,
|
|
pin_peer_name=pin_peer_name,
|
|
user_peer_aliases=user_peer_aliases or {},
|
|
runtime_peer_prefix=runtime_peer_prefix,
|
|
session_peer_prefix=session_peer_prefix,
|
|
enabled=False,
|
|
write_frequency="turn", # avoid spawning the async writer thread
|
|
)
|
|
|
|
def test_runtime_wins_when_pin_is_false(self):
|
|
"""Regression guard: default behaviour must stay unchanged.
|
|
Multi-user bots rely on the platform-native ID winning."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(peer_name="Igor", pin_peer_name=False),
|
|
runtime_user_peer_name="86701400", # e.g. Telegram UID
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "86701400", (
|
|
"pin_peer_name=False is the multi-user default — the gateway's "
|
|
"platform-native user ID must win so each user gets their own "
|
|
"peer scope. If this regresses, every Telegram/Discord/Slack "
|
|
"bot immediately merges memory across users."
|
|
)
|
|
|
|
def test_alias_wins_for_known_runtime_id(self):
|
|
"""Known platform IDs can preserve an existing stable Honcho peer."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name="Igor",
|
|
pin_peer_name=False,
|
|
user_peer_aliases={"86701400": "Igor"},
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "Igor"
|
|
|
|
def test_unknown_runtime_id_uses_prefix(self):
|
|
"""Unknown gateway users stay isolated but become platform-scoped."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name="Igor",
|
|
pin_peer_name=False,
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "telegram_86701400"
|
|
|
|
def test_prefixed_runtime_id_hashes_when_sanitization_is_lossy(self):
|
|
"""Generated prefixed IDs avoid merges caused by lossy sanitization."""
|
|
raw_peer_id = "telegram_user:42"
|
|
expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8]
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="user:42",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:user:42")
|
|
assert session.user_peer_id == f"telegram_user-42-{expected_hash}"
|
|
|
|
def test_prefixed_runtime_id_hashes_when_it_collides_with_peer_name(self):
|
|
"""Unknown generated peers should not silently merge into peerName."""
|
|
raw_peer_id = "telegram_86701400"
|
|
expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8]
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name="telegram_86701400",
|
|
pin_peer_name=False,
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == f"telegram_86701400-{expected_hash}"
|
|
|
|
def test_prefixed_runtime_id_hashes_when_it_collides_with_alias_target(self):
|
|
"""Unknown generated peers should not silently merge into alias targets."""
|
|
raw_peer_id = "telegram_86701400"
|
|
expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8]
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
user_peer_aliases={"known-user": "telegram_86701400"},
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == f"telegram_86701400-{expected_hash}"
|
|
|
|
def test_prefixed_runtime_id_extends_hash_when_short_hash_collides(self):
|
|
raw_peer_id = "telegram_86701400"
|
|
digest = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
user_peer_aliases={
|
|
"known-user": "telegram_86701400",
|
|
"reserved-user": f"telegram_86701400-{digest[:8]}",
|
|
},
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == f"telegram_86701400-{digest[:12]}"
|
|
|
|
def test_alias_value_is_sanitized_after_selection(self):
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
user_peer_aliases={"86701400": "Alice Smith!"},
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "Alice-Smith-"
|
|
|
|
def test_alias_keys_match_raw_runtime_id_before_sanitization(self):
|
|
"""Alias selection is exact on platform IDs before Honcho ID cleanup."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
user_peer_aliases={
|
|
"user:42": "raw-match",
|
|
"user-42": "sanitized-match",
|
|
},
|
|
),
|
|
runtime_user_peer_name="user:42",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:user:42")
|
|
assert session.user_peer_id == "raw-match"
|
|
|
|
def test_session_peer_prefix_is_orthogonal_to_runtime_peer_prefix(self):
|
|
"""sessionPeerPrefix scopes session IDs; runtimePeerPrefix scopes user peers."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name="Igor",
|
|
pin_peer_name=False,
|
|
runtime_peer_prefix="telegram_",
|
|
session_peer_prefix=True,
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "telegram_86701400"
|
|
assert session.honcho_session_id == "telegram-86701400"
|
|
|
|
def test_config_wins_when_pin_is_true(self):
|
|
"""With pin enabled, configured peer_name beats runtime ID."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name="Igor",
|
|
pin_peer_name=True,
|
|
user_peer_aliases={"86701400": "Alias"},
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400", # Telegram pushes this in
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "Igor", (
|
|
"With pinPeerName=true the user's configured peer_name must "
|
|
"beat the platform-native runtime ID so memory stays unified "
|
|
"across Telegram/Discord/Slack for the same person."
|
|
)
|
|
|
|
def test_pin_noop_when_peer_name_missing(self):
|
|
"""Safety: pinPeerName alone (no peer_name) must not silently drop
|
|
the runtime identity. Without a configured peer_name there's
|
|
nothing to pin to — fall through to runtime mapping."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=True,
|
|
user_peer_aliases={"86701400": "Igor"},
|
|
runtime_peer_prefix="telegram_",
|
|
),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "Igor"
|
|
|
|
def test_pin_noop_without_peer_name_or_mapping_preserves_runtime(self):
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(peer_name=None, pin_peer_name=True),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "86701400"
|
|
|
|
def test_alt_runtime_id_can_match_alias_without_changing_raw_fallback(self):
|
|
"""Stable alternate IDs can map known users while primary ID fallback stays unchanged."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
user_peer_aliases={"union-user": "Igor"},
|
|
runtime_peer_prefix="feishu_",
|
|
),
|
|
runtime_user_peer_name="open-id",
|
|
runtime_user_peer_name_alt="union-user",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("feishu:chat")
|
|
assert session.user_peer_id == "Igor"
|
|
|
|
def test_alt_runtime_id_does_not_replace_primary_prefix_fallback(self):
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(
|
|
peer_name=None,
|
|
pin_peer_name=False,
|
|
user_peer_aliases={"other-union": "Igor"},
|
|
runtime_peer_prefix="feishu_",
|
|
),
|
|
runtime_user_peer_name="open-id",
|
|
runtime_user_peer_name_alt="union-user",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("feishu:chat")
|
|
assert session.user_peer_id == "feishu_open-id"
|
|
|
|
def test_runtime_missing_falls_back_to_peer_name(self):
|
|
"""CLI-mode (no gateway runtime identity) uses config peer_name —
|
|
this path was already correct but the refactor shouldn't break it."""
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(peer_name="Igor", pin_peer_name=False),
|
|
runtime_user_peer_name=None,
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("cli:local")
|
|
assert session.user_peer_id == "Igor"
|
|
|
|
def test_everything_missing_falls_back_to_session_key(self):
|
|
"""Deepest fallback: no runtime identity, no peer_name, no pin.
|
|
Must still produce a deterministic peer_id from the session key."""
|
|
# Config with no peer_name and default pin_peer_name=False
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config(peer_name=None, pin_peer_name=False),
|
|
runtime_user_peer_name=None,
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:123")
|
|
assert session.user_peer_id == "user-telegram-123"
|
|
|
|
def test_pin_does_not_affect_assistant_peer(self):
|
|
"""The flag only pins the USER peer — the assistant peer continues
|
|
to come from ``ai_peer`` and must not be touched."""
|
|
cfg = HonchoClientConfig(
|
|
api_key="k",
|
|
peer_name="Igor",
|
|
pin_peer_name=True,
|
|
ai_peer="hermes-assistant",
|
|
enabled=False,
|
|
write_frequency="turn",
|
|
)
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=cfg,
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
|
|
session = mgr.get_or_create("telegram:86701400")
|
|
assert session.user_peer_id == "Igor"
|
|
assert session.assistant_peer_id == "hermes-assistant"
|
|
|
|
|
|
class TestCrossPlatformMemoryUnification:
|
|
"""The same physical user talking to Hermes via Telegram AND Discord
|
|
lands on ONE peer when ``pinPeerName`` is opted in.
|
|
"""
|
|
|
|
def _config_pinned(self) -> HonchoClientConfig:
|
|
return HonchoClientConfig(
|
|
api_key="k",
|
|
peer_name="Igor",
|
|
pin_peer_name=True,
|
|
enabled=False,
|
|
write_frequency="turn",
|
|
)
|
|
|
|
def test_telegram_and_discord_collapse_to_one_peer_when_pinned(self):
|
|
"""Single-user deployment: Telegram UID and Discord snowflake
|
|
both resolve to the same configured peer_name."""
|
|
# Telegram turn
|
|
mgr_telegram = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config_pinned(),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr_telegram)
|
|
telegram_session = mgr_telegram.get_or_create("telegram:86701400")
|
|
|
|
# Discord turn (separate manager instance — simulates a fresh
|
|
# platform-adapter invocation)
|
|
mgr_discord = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._config_pinned(),
|
|
runtime_user_peer_name="1348750102029926454",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr_discord)
|
|
discord_session = mgr_discord.get_or_create("discord:1348750102029926454")
|
|
|
|
assert telegram_session.user_peer_id == "Igor"
|
|
assert discord_session.user_peer_id == "Igor"
|
|
assert telegram_session.user_peer_id == discord_session.user_peer_id, (
|
|
"cross-platform memory unification is the whole point of "
|
|
"pinPeerName — both platforms must land on the same Honcho peer"
|
|
)
|
|
|
|
def test_multiuser_default_keeps_platforms_separate(self):
|
|
"""Negative control: with pinPeerName=false (the default), two
|
|
different platform IDs must produce two different peers so
|
|
multi-user bots don't merge users."""
|
|
cfg = HonchoClientConfig(
|
|
api_key="k",
|
|
peer_name="Igor",
|
|
pin_peer_name=False,
|
|
enabled=False,
|
|
write_frequency="turn",
|
|
)
|
|
mgr_a = HonchoSessionManager(
|
|
honcho=MagicMock(), config=cfg, runtime_user_peer_name="user_a",
|
|
)
|
|
mgr_b = HonchoSessionManager(
|
|
honcho=MagicMock(), config=cfg, runtime_user_peer_name="user_b",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr_a)
|
|
_patch_manager_for_resolution_test(mgr_b)
|
|
|
|
sess_a = mgr_a.get_or_create("telegram:a")
|
|
sess_b = mgr_b.get_or_create("telegram:b")
|
|
|
|
assert sess_a.user_peer_id == "user_a"
|
|
assert sess_b.user_peer_id == "user_b"
|
|
assert sess_a.user_peer_id != sess_b.user_peer_id, (
|
|
"multi-user default MUST keep users separate — a regression "
|
|
"here would silently merge unrelated users' memory"
|
|
)
|
|
|
|
|
|
class TestPinUserPeerAlias:
|
|
"""``pinUserPeer`` and ``pinPeerName`` both resolve to the same internal
|
|
``pin_peer_name`` field. Precedence when both appear: host pinUserPeer →
|
|
host pinPeerName → root pinUserPeer → root pinPeerName → default.
|
|
"""
|
|
|
|
def test_root_pinUserPeer_true_pins(self, tmp_path):
|
|
from plugins.memory.honcho.client import HonchoClientConfig
|
|
import json
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "***",
|
|
"peerName": "eri",
|
|
"pinUserPeer": True,
|
|
}))
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is True
|
|
|
|
def test_host_pinUserPeer_wins_over_root_pinPeerName(self, tmp_path):
|
|
from plugins.memory.honcho.client import HonchoClientConfig
|
|
import json
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "***",
|
|
"peerName": "eri",
|
|
"pinPeerName": False,
|
|
"hosts": {"hermes": {"pinUserPeer": True}},
|
|
}))
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is True
|
|
|
|
def test_host_pinUserPeer_false_disables_root_pinPeerName(self, tmp_path):
|
|
from plugins.memory.honcho.client import HonchoClientConfig
|
|
import json
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "***",
|
|
"peerName": "eri",
|
|
"pinPeerName": True,
|
|
"hosts": {"hermes": {"pinUserPeer": False}},
|
|
}))
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is False, (
|
|
"Host-level pinUserPeer=false must override root-level "
|
|
"pinPeerName=true so a host can unpin a globally-pinned profile."
|
|
)
|
|
|
|
def test_pinPeerName_still_works_unchanged(self, tmp_path):
|
|
from plugins.memory.honcho.client import HonchoClientConfig
|
|
import json
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "***",
|
|
"peerName": "eri",
|
|
"hosts": {"hermes": {"pinPeerName": True}},
|
|
}))
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
assert config.pin_peer_name is True
|
|
|
|
|
|
class TestPinTransition:
|
|
"""Behavior when honcho.json flips ``pinPeerName`` true → false.
|
|
|
|
Covers two contracts:
|
|
1. A freshly-built manager picks up the flipped config and resolves
|
|
the same runtime ID to a new peer (no resolver staleness).
|
|
2. The gateway's agent-cache signature reflects honcho identity-mapping
|
|
changes, so a config edit busts the cached AIAgent on the next turn.
|
|
"""
|
|
|
|
def _pinned(self) -> HonchoClientConfig:
|
|
return HonchoClientConfig(
|
|
api_key="k",
|
|
peer_name="Igor",
|
|
pin_peer_name=True,
|
|
enabled=False,
|
|
write_frequency="turn",
|
|
)
|
|
|
|
def _unpinned(self) -> HonchoClientConfig:
|
|
return HonchoClientConfig(
|
|
api_key="k",
|
|
peer_name="Igor",
|
|
pin_peer_name=False,
|
|
enabled=False,
|
|
write_frequency="turn",
|
|
)
|
|
|
|
def test_fresh_manager_after_flip_resolves_to_runtime(self):
|
|
pinned_mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._pinned(),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(pinned_mgr)
|
|
before = pinned_mgr.get_or_create("telegram:86701400")
|
|
assert before.user_peer_id == "Igor"
|
|
|
|
unpinned_mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._unpinned(),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(unpinned_mgr)
|
|
after = unpinned_mgr.get_or_create("telegram:86701400")
|
|
assert after.user_peer_id == "86701400", (
|
|
"After flipping pinPeerName off, the same runtime ID must resolve "
|
|
"to its own peer — otherwise multi-user mode silently merges users."
|
|
)
|
|
|
|
def test_cached_session_survives_config_flip_in_same_manager(self):
|
|
mgr = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._pinned(),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr)
|
|
first = mgr.get_or_create("telegram:86701400")
|
|
assert first.user_peer_id == "Igor"
|
|
|
|
mgr._config = self._unpinned()
|
|
second = mgr.get_or_create("telegram:86701400")
|
|
assert second.user_peer_id == "Igor", (
|
|
"The per-key session cache is keyed by session-key, not by "
|
|
"resolved peer. In-process flips don't invalidate it — the "
|
|
"gateway cache must bust the whole manager instead."
|
|
)
|
|
|
|
def test_cache_busting_signature_reflects_pin_peer_name(self, tmp_path, monkeypatch):
|
|
"""Gateway agent cache must bust when honcho.json's pinPeerName flips."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
cfg_path = tmp_path / "honcho.json"
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": True}))
|
|
sig_pinned = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": False}))
|
|
sig_unpinned = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
assert sig_pinned["honcho.pin_peer_name"] != sig_unpinned["honcho.pin_peer_name"]
|
|
|
|
def test_cache_busting_signature_reflects_user_peer_aliases(self, tmp_path, monkeypatch):
|
|
from gateway.run import GatewayRunner
|
|
|
|
cfg_path = tmp_path / "honcho.json"
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
|
|
sig_no_aliases = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
cfg_path.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"userPeerAliases": {"86701400": "Igor"},
|
|
}))
|
|
sig_with_aliases = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
assert sig_no_aliases["honcho.user_peer_aliases"] != sig_with_aliases["honcho.user_peer_aliases"]
|
|
|
|
def test_cache_busting_signature_reflects_runtime_peer_prefix(self, tmp_path, monkeypatch):
|
|
from gateway.run import GatewayRunner
|
|
|
|
cfg_path = tmp_path / "honcho.json"
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
|
|
sig_no_prefix = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
cfg_path.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"runtimePeerPrefix": "telegram_",
|
|
}))
|
|
sig_with_prefix = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
assert sig_no_prefix["honcho.runtime_peer_prefix"] != sig_with_prefix["honcho.runtime_peer_prefix"]
|
|
|
|
def test_cache_busting_signature_reflects_ai_peer(self, tmp_path, monkeypatch):
|
|
"""Editing ``aiPeer`` mid-flight must invalidate the cached agent.
|
|
|
|
``HonchoSessionManager`` freezes ``cfg.ai_peer`` at construction —
|
|
without busting here, assistant writes keep landing on the old
|
|
peer until an unrelated cache eviction.
|
|
"""
|
|
from gateway.run import GatewayRunner
|
|
|
|
cfg_path = tmp_path / "honcho.json"
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
cfg_path.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"aiPeer": "hermes",
|
|
}))
|
|
sig_before = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
cfg_path.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "Igor",
|
|
"aiPeer": "hermetika",
|
|
}))
|
|
sig_after = GatewayRunner._extract_cache_busting_config({})
|
|
|
|
assert sig_before["honcho.ai_peer"] != sig_after["honcho.ai_peer"]
|
|
|
|
|
|
class TestProfilePeerUniqueness:
|
|
"""Each Hermes profile can pin to its own unique peerName.
|
|
|
|
Profile cloning copies host blocks, but operators routinely diverge them
|
|
afterwards (e.g. `hermes -p partner` pinned to a different person's peer).
|
|
The resolver must honor host-level ``peerName`` so two profiles in the
|
|
same workspace stay scoped to different Honcho peers.
|
|
"""
|
|
|
|
def _pinned_to(self, name: str) -> HonchoClientConfig:
|
|
return HonchoClientConfig(
|
|
api_key="k",
|
|
peer_name=name,
|
|
pin_peer_name=True,
|
|
enabled=False,
|
|
write_frequency="turn",
|
|
)
|
|
|
|
def test_two_profiles_pinned_to_different_peer_names_resolve_distinctly(self):
|
|
mgr_a = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._pinned_to("alice"),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr_a)
|
|
sess_a = mgr_a.get_or_create("telegram:86701400")
|
|
|
|
mgr_b = HonchoSessionManager(
|
|
honcho=MagicMock(),
|
|
config=self._pinned_to("bob"),
|
|
runtime_user_peer_name="86701400",
|
|
)
|
|
_patch_manager_for_resolution_test(mgr_b)
|
|
sess_b = mgr_b.get_or_create("telegram:86701400")
|
|
|
|
assert sess_a.user_peer_id == "alice"
|
|
assert sess_b.user_peer_id == "bob"
|
|
assert sess_a.user_peer_id != sess_b.user_peer_id, (
|
|
"Profiles pinned to distinct peer names must not collapse to "
|
|
"the same Honcho peer — otherwise profile isolation is fictional."
|
|
)
|
|
|
|
def test_host_peer_name_overrides_root_when_pinned(self, tmp_path, monkeypatch):
|
|
"""Host-level peerName wins so each profile can pin uniquely while
|
|
sharing a single root-level apiKey and workspace.
|
|
"""
|
|
config_file = tmp_path / "honcho.json"
|
|
config_file.write_text(json.dumps({
|
|
"apiKey": "k",
|
|
"peerName": "default-user",
|
|
"hosts": {
|
|
"hermes.partner": {
|
|
"peerName": "partner-user",
|
|
"pinPeerName": True,
|
|
},
|
|
},
|
|
}))
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
|
|
|
|
cfg = HonchoClientConfig.from_global_config(
|
|
host="hermes.partner", config_path=config_file,
|
|
)
|
|
assert cfg.peer_name == "partner-user"
|
|
assert cfg.pin_peer_name is True
|