fix(honcho): harden tool gating and migration peer routing
Prevent stale Honcho tool exposure in context/local modes, restore reliable async write retry behavior, and ensure SOUL.md migration uploads target the AI peer instead of the user peer. Also align Honcho CLI key checks with host-scoped apiKey resolution and lock the fixes with regression tests. Made-with: Cursor
This commit is contained in:
parent
8cddcfa0d8
commit
2d35016b94
6 changed files with 297 additions and 56 deletions
|
|
@ -380,10 +380,10 @@ class TestAsyncWriterThread:
|
|||
sess.add_message("user", "async msg")
|
||||
|
||||
flushed = []
|
||||
original = mgr._flush_session
|
||||
|
||||
def capture(s):
|
||||
flushed.append(s)
|
||||
return True
|
||||
|
||||
mgr._flush_session = capture
|
||||
mgr._async_queue.put(sess)
|
||||
|
|
@ -457,6 +457,66 @@ class TestAsyncWriterRetry:
|
|||
assert call_count[0] == 2
|
||||
assert not mgr._async_thread.is_alive()
|
||||
|
||||
def test_retries_when_flush_reports_failure(self):
|
||||
mgr = _make_manager(write_frequency="async")
|
||||
sess = _make_session()
|
||||
sess.add_message("user", "msg")
|
||||
|
||||
call_count = [0]
|
||||
|
||||
def fail_then_succeed(_session):
|
||||
call_count[0] += 1
|
||||
return call_count[0] > 1
|
||||
|
||||
mgr._flush_session = fail_then_succeed
|
||||
|
||||
with patch("time.sleep"):
|
||||
mgr._async_queue.put(sess)
|
||||
deadline = time.time() + 3.0
|
||||
while call_count[0] < 2 and time.time() < deadline:
|
||||
time.sleep(0.05)
|
||||
|
||||
mgr.shutdown()
|
||||
assert call_count[0] == 2
|
||||
|
||||
|
||||
class TestMemoryFileMigrationTargets:
|
||||
def test_soul_upload_targets_ai_peer(self, tmp_path):
|
||||
mgr = _make_manager(write_frequency="turn")
|
||||
session = _make_session(
|
||||
key="cli:test",
|
||||
user_peer_id="custom-user",
|
||||
assistant_peer_id="custom-ai",
|
||||
honcho_session_id="cli-test",
|
||||
)
|
||||
mgr._cache[session.key] = session
|
||||
|
||||
user_peer = MagicMock(name="user-peer")
|
||||
ai_peer = MagicMock(name="ai-peer")
|
||||
mgr._peers_cache[session.user_peer_id] = user_peer
|
||||
mgr._peers_cache[session.assistant_peer_id] = ai_peer
|
||||
|
||||
honcho_session = MagicMock()
|
||||
mgr._sessions_cache[session.honcho_session_id] = honcho_session
|
||||
|
||||
(tmp_path / "MEMORY.md").write_text("memory facts", encoding="utf-8")
|
||||
(tmp_path / "USER.md").write_text("user profile", encoding="utf-8")
|
||||
(tmp_path / "SOUL.md").write_text("ai identity", encoding="utf-8")
|
||||
|
||||
uploaded = mgr.migrate_memory_files(session.key, str(tmp_path))
|
||||
|
||||
assert uploaded is True
|
||||
assert honcho_session.upload_file.call_count == 3
|
||||
|
||||
peer_by_upload_name = {}
|
||||
for call_args in honcho_session.upload_file.call_args_list:
|
||||
payload = call_args.kwargs["file"]
|
||||
peer_by_upload_name[payload[0]] = call_args.kwargs["peer"]
|
||||
|
||||
assert peer_by_upload_name["consolidated_memory.md"] is user_peer
|
||||
assert peer_by_upload_name["user_profile.md"] is user_peer
|
||||
assert peer_by_upload_name["agent_soul.md"] is ai_peer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HonchoClientConfig dataclass defaults for new fields
|
||||
|
|
|
|||
29
tests/honcho_integration/test_cli.py
Normal file
29
tests/honcho_integration/test_cli.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Tests for Honcho CLI helpers."""
|
||||
|
||||
from honcho_integration.cli import _resolve_api_key
|
||||
|
||||
|
||||
class TestResolveApiKey:
|
||||
def test_prefers_host_scoped_key(self):
|
||||
cfg = {
|
||||
"apiKey": "root-key",
|
||||
"hosts": {
|
||||
"hermes": {
|
||||
"apiKey": "host-key",
|
||||
}
|
||||
},
|
||||
}
|
||||
assert _resolve_api_key(cfg) == "host-key"
|
||||
|
||||
def test_falls_back_to_root_key(self):
|
||||
cfg = {
|
||||
"apiKey": "root-key",
|
||||
"hosts": {"hermes": {}},
|
||||
}
|
||||
assert _resolve_api_key(cfg) == "root-key"
|
||||
|
||||
def test_falls_back_to_env_key(self, monkeypatch):
|
||||
monkeypatch.setenv("HONCHO_API_KEY", "env-key")
|
||||
assert _resolve_api_key({}) == "env-key"
|
||||
monkeypatch.delenv("HONCHO_API_KEY", raising=False)
|
||||
|
||||
|
|
@ -1277,6 +1277,82 @@ class TestHonchoActivation:
|
|||
)
|
||||
mock_client.assert_not_called()
|
||||
|
||||
def test_recall_mode_context_suppresses_honcho_tools(self):
|
||||
hcfg = HonchoClientConfig(
|
||||
enabled=True,
|
||||
api_key="honcho-key",
|
||||
memory_mode="hybrid",
|
||||
peer_name="user",
|
||||
ai_peer="hermes",
|
||||
recall_mode="context",
|
||||
)
|
||||
manager = MagicMock()
|
||||
manager._config = hcfg
|
||||
manager.get_or_create.return_value = SimpleNamespace(messages=[])
|
||||
manager.get_prefetch_context.return_value = {"representation": "Known user", "card": ""}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"run_agent.get_tool_definitions",
|
||||
side_effect=[
|
||||
_make_tool_defs("web_search"),
|
||||
_make_tool_defs(
|
||||
"web_search",
|
||||
"honcho_context",
|
||||
"honcho_profile",
|
||||
"honcho_search",
|
||||
"honcho_conclude",
|
||||
),
|
||||
],
|
||||
),
|
||||
patch("run_agent.check_toolset_requirements", return_value={}),
|
||||
patch("run_agent.OpenAI"),
|
||||
patch("tools.honcho_tools.set_session_context"),
|
||||
):
|
||||
agent = AIAgent(
|
||||
api_key="test-key-1234567890",
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=False,
|
||||
honcho_session_key="gateway-session",
|
||||
honcho_manager=manager,
|
||||
honcho_config=hcfg,
|
||||
)
|
||||
|
||||
assert "web_search" in agent.valid_tool_names
|
||||
assert "honcho_context" not in agent.valid_tool_names
|
||||
assert "honcho_profile" not in agent.valid_tool_names
|
||||
assert "honcho_search" not in agent.valid_tool_names
|
||||
assert "honcho_conclude" not in agent.valid_tool_names
|
||||
|
||||
def test_inactive_honcho_strips_stale_honcho_tools(self):
|
||||
hcfg = HonchoClientConfig(
|
||||
enabled=True,
|
||||
api_key="honcho-key",
|
||||
memory_mode="local",
|
||||
peer_name="user",
|
||||
ai_peer="hermes",
|
||||
)
|
||||
|
||||
with (
|
||||
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search", "honcho_context")),
|
||||
patch("run_agent.check_toolset_requirements", return_value={}),
|
||||
patch("run_agent.OpenAI"),
|
||||
patch("honcho_integration.client.HonchoClientConfig.from_global_config", return_value=hcfg),
|
||||
patch("honcho_integration.client.get_honcho_client") as mock_client,
|
||||
):
|
||||
agent = AIAgent(
|
||||
api_key="test-key-1234567890",
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=False,
|
||||
)
|
||||
|
||||
assert agent._honcho is None
|
||||
assert "web_search" in agent.valid_tool_names
|
||||
assert "honcho_context" not in agent.valid_tool_names
|
||||
mock_client.assert_not_called()
|
||||
|
||||
|
||||
class TestHonchoPrefetchScheduling:
|
||||
def test_honcho_prefetch_includes_cached_dialectic(self, agent):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue