hermes-agent/tests/gateway/test_multi_agent_integratio...

246 lines
9.4 KiB
Python

"""Tests for multi-agent integration with existing components.
Covers:
1. Session key namespacing via build_session_key
2. ToolPolicy filtering via get_tool_definitions(agent_tool_policy=...)
3. MemoryStore with custom memory_dir
4. DEFAULT_CONFIG shape (agents, bindings, _config_version)
5. /agents command presence in COMMANDS dict
"""
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from gateway.config import Platform
from gateway.session import SessionSource, build_session_key
from gateway.agent_registry import ToolPolicy
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_source(platform=Platform.TELEGRAM, chat_id="12345",
chat_type="dm", user_id=None):
return SessionSource(
platform=platform,
chat_id=chat_id,
chat_type=chat_type,
user_id=user_id,
)
# ===================================================================
# 1. Session key namespacing
# ===================================================================
class TestBuildSessionKeyNamespacing:
"""build_session_key must produce distinct keys for different agent_ids."""
def test_different_agent_ids_produce_different_keys(self):
source = _make_source()
key_main = build_session_key(source, agent_id="main")
key_helper = build_session_key(source, agent_id="helper")
assert key_main != key_helper
assert "agent:main:" in key_main
assert "agent:helper:" in key_helper
def test_backward_compat_main_agent(self):
"""agent_id='main' (the default) produces 'agent:main:<platform>:dm'."""
source = _make_source(platform=Platform.TELEGRAM)
key = build_session_key(source) # defaults to agent_id='main'
assert key == "agent:main:telegram:dm"
def test_backward_compat_main_group(self):
source = _make_source(platform=Platform.DISCORD, chat_type="group",
chat_id="guild-abc")
key = build_session_key(source)
assert key == "agent:main:discord:group:guild-abc"
def test_agent_id_embedded_in_group_key(self):
source = _make_source(platform=Platform.DISCORD, chat_type="group",
chat_id="guild-abc")
key = build_session_key(source, agent_id="code-review")
assert key == "agent:code-review:discord:group:guild-abc"
def test_dm_scope_per_peer_includes_user_id(self):
source = _make_source(user_id="user-42")
key = build_session_key(source, dm_scope="per_peer")
assert "user-42" in key
assert key == "agent:main:telegram:dm:user-42"
def test_dm_scope_per_peer_no_user_id_falls_back(self):
"""When user_id is None, per_peer falls back to the plain DM key."""
source = _make_source(user_id=None)
key = build_session_key(source, dm_scope="per_peer")
assert key == "agent:main:telegram:dm"
def test_dm_scope_default_ignores_user_id(self):
"""Default dm_scope='main' does NOT include user_id."""
source = _make_source(user_id="user-42")
key = build_session_key(source, dm_scope="main")
assert "user-42" not in key
def test_whatsapp_dm_includes_chat_id(self):
"""WhatsApp DMs always include chat_id (multi-user device)."""
source = _make_source(platform=Platform.WHATSAPP, chat_id="wa-phone")
key = build_session_key(source, agent_id="main")
assert key == "agent:main:whatsapp:dm:wa-phone"
# ===================================================================
# 2. ToolPolicy filtering in get_tool_definitions
# ===================================================================
class TestToolPolicyFiltering:
"""get_tool_definitions should honour agent_tool_policy when provided."""
@staticmethod
def _make_tool_def(name):
return {
"type": "function",
"function": {
"name": name,
"description": f"Stub for {name}",
"parameters": {"type": "object", "properties": {}},
},
}
def test_deny_removes_tool(self):
"""A ToolPolicy with deny=['terminal'] should remove terminal."""
policy = ToolPolicy(deny=["terminal"])
all_tools = {"terminal", "memory", "web_search"}
result = policy.apply(all_tools)
assert "terminal" not in result
assert "memory" in result
assert "web_search" in result
def test_allow_restricts_to_listed(self):
policy = ToolPolicy(allow=["memory", "web_search"])
all_tools = {"terminal", "memory", "web_search", "read_file"}
result = policy.apply(all_tools)
assert result == {"memory", "web_search"}
def test_profile_minimal(self):
"""The 'minimal' profile only keeps its allow list."""
policy = ToolPolicy(profile="minimal")
all_tools = {"terminal", "memory", "clarify", "todo", "session_search",
"web_search", "read_file"}
result = policy.apply(all_tools)
assert result == {"memory", "clarify", "todo", "session_search"}
def test_deny_overrides_allow(self):
"""Deny always wins, even if the tool is in the allow list."""
policy = ToolPolicy(allow=["memory", "terminal"], deny=["terminal"])
all_tools = {"terminal", "memory", "web_search"}
result = policy.apply(all_tools)
assert result == {"memory"}
@patch("model_tools.registry")
@patch("model_tools.resolve_toolset")
@patch("model_tools.validate_toolset", return_value=True)
def test_get_tool_definitions_applies_policy(self, mock_validate,
mock_resolve, mock_reg):
"""End-to-end: get_tool_definitions respects agent_tool_policy."""
from model_tools import get_tool_definitions
mock_resolve.return_value = ["terminal", "memory", "web_search"]
mock_reg.get_definitions.return_value = [
self._make_tool_def("memory"),
self._make_tool_def("web_search"),
]
policy = ToolPolicy(deny=["terminal"])
tools = get_tool_definitions(
enabled_toolsets=["hermes-cli"],
quiet_mode=True,
agent_tool_policy=policy,
)
# registry.get_definitions should have been called with a set
# that does NOT contain 'terminal'
called_tools = mock_reg.get_definitions.call_args[0][0]
assert "terminal" not in called_tools
assert "memory" in called_tools
# ===================================================================
# 3. MemoryStore with custom memory_dir
# ===================================================================
class TestMemoryStoreCustomDir:
"""MemoryStore should use a custom memory_dir when provided."""
def test_custom_dir_is_used(self, tmp_path):
custom = tmp_path / "custom_memories"
# MemoryStore.__init__ creates the directory
from tools.memory_tool import MemoryStore
store = MemoryStore(memory_dir=custom)
assert store._memory_dir == custom
assert custom.exists()
def test_default_dir_is_global(self):
"""Without memory_dir, the store falls back to MEMORY_DIR."""
from tools.memory_tool import MemoryStore, MEMORY_DIR
with patch.object(Path, "mkdir"): # avoid touching real FS
store = MemoryStore()
assert store._memory_dir == MEMORY_DIR
def test_load_and_save_use_custom_dir(self, tmp_path):
custom = tmp_path / "mem"
from tools.memory_tool import MemoryStore
store = MemoryStore(memory_dir=custom)
store.load_from_disk() # should not raise
assert (custom).exists()
# Save should write to custom dir
store.memory_entries = ["fact one"]
store.save_to_disk("memory")
assert (custom / "MEMORY.md").exists()
# ===================================================================
# 4. Config shape
# ===================================================================
class TestDefaultConfigShape:
"""DEFAULT_CONFIG must contain multi-agent keys."""
def test_agents_key_exists(self):
from hermes_cli.config import DEFAULT_CONFIG
assert "agents" in DEFAULT_CONFIG
def test_bindings_key_exists(self):
from hermes_cli.config import DEFAULT_CONFIG
assert "bindings" in DEFAULT_CONFIG
def test_agents_default_is_empty_dict(self):
from hermes_cli.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["agents"] == {}
def test_bindings_default_is_empty_list(self):
from hermes_cli.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["bindings"] == []
def test_config_version_is_7(self):
from hermes_cli.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["_config_version"] == 7
# ===================================================================
# 5. /agents command in COMMANDS dict
# ===================================================================
class TestAgentsCommand:
"""/agents must be registered in the COMMANDS dict."""
def test_agents_in_commands(self):
from hermes_cli.commands import COMMANDS
assert "/agents" in COMMANDS
def test_agents_has_description(self):
from hermes_cli.commands import COMMANDS
desc = COMMANDS["/agents"]
assert isinstance(desc, str)
assert len(desc) > 0