hermes-agent/tests/honcho_plugin/test_pin_peer_name.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

574 lines
22 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_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_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="7654321", # e.g. Telegram UID
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
assert session.user_peer_id == "7654321", (
"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={"7654321": "Igor"},
runtime_peer_prefix="telegram_",
),
runtime_user_peer_name="7654321",
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
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="7654321",
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
assert session.user_peer_id == "telegram_7654321"
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_7654321"
expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8]
mgr = HonchoSessionManager(
honcho=MagicMock(),
config=self._config(
peer_name="telegram_7654321",
pin_peer_name=False,
runtime_peer_prefix="telegram_",
),
runtime_user_peer_name="7654321",
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
assert session.user_peer_id == f"telegram_7654321-{expected_hash}"
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={"7654321": "Alice Smith!"},
),
runtime_user_peer_name="7654321",
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
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="7654321",
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
assert session.user_peer_id == "telegram_7654321"
assert session.honcho_session_id == "telegram-7654321"
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={"7654321": "Alias"},
runtime_peer_prefix="telegram_",
),
runtime_user_peer_name="7654321", # Telegram pushes this in
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
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={"7654321": "Igor"},
runtime_peer_prefix="telegram_",
),
runtime_user_peer_name="7654321",
)
_patch_manager_for_resolution_test(mgr)
session = mgr.get_or_create("telegram:7654321")
assert session.user_peer_id == "Igor"
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_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"
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="7654321",
)
_patch_manager_for_resolution_test(mgr_telegram)
telegram_session = mgr_telegram.get_or_create("telegram:7654321")
# 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_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="7654321",
)
_patch_manager_for_resolution_test(pinned_mgr)
before = pinned_mgr.get_or_create("telegram:7654321")
assert before.user_peer_id == "Igor"
unpinned_mgr = HonchoSessionManager(
honcho=MagicMock(),
config=self._unpinned(),
runtime_user_peer_name="7654321",
)
_patch_manager_for_resolution_test(unpinned_mgr)
after = unpinned_mgr.get_or_create("telegram:7654321")
assert after.user_peer_id == "7654321", (
"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="7654321",
)
_patch_manager_for_resolution_test(mgr)
first = mgr.get_or_create("telegram:7654321")
assert first.user_peer_id == "Igor"
mgr._config = self._unpinned()
second = mgr.get_or_create("telegram:7654321")
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({"memory": {"provider": "honcho"}})
cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": False}))
sig_unpinned = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
assert sig_pinned["honcho.pin_peer_name"] != sig_unpinned["honcho.pin_peer_name"]
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="7654321",
)
_patch_manager_for_resolution_test(mgr_a)
sess_a = mgr_a.get_or_create("telegram:7654321")
mgr_b = HonchoSessionManager(
honcho=MagicMock(),
config=self._pinned_to("bob"),
runtime_user_peer_name="7654321",
)
_patch_manager_for_resolution_test(mgr_b)
sess_b = mgr_b.get_or_create("telegram:7654321")
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."
)