test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
"""Tests for honcho_integration/client.py — Honcho client configuration."""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from honcho_integration.client import (
|
|
|
|
|
HonchoClientConfig,
|
|
|
|
|
get_honcho_client,
|
|
|
|
|
reset_honcho_client,
|
|
|
|
|
GLOBAL_CONFIG_PATH,
|
|
|
|
|
HOST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestHonchoClientConfigDefaults:
|
|
|
|
|
def test_default_values(self):
|
|
|
|
|
config = HonchoClientConfig()
|
|
|
|
|
assert config.host == "hermes"
|
|
|
|
|
assert config.workspace_id == "hermes"
|
|
|
|
|
assert config.api_key is None
|
|
|
|
|
assert config.environment == "production"
|
|
|
|
|
assert config.enabled is False
|
|
|
|
|
assert config.save_messages is True
|
2026-03-11 15:42:35 -04:00
|
|
|
assert config.session_strategy == "per-session"
|
2026-03-09 17:46:51 -04:00
|
|
|
assert config.recall_mode == "hybrid"
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
assert config.session_peer_prefix is False
|
|
|
|
|
assert config.linked_hosts == []
|
|
|
|
|
assert config.sessions == {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFromEnv:
|
|
|
|
|
def test_reads_api_key_from_env(self):
|
|
|
|
|
with patch.dict(os.environ, {"HONCHO_API_KEY": "test-key-123"}):
|
|
|
|
|
config = HonchoClientConfig.from_env()
|
|
|
|
|
assert config.api_key == "test-key-123"
|
|
|
|
|
assert config.enabled is True
|
|
|
|
|
|
|
|
|
|
def test_reads_environment_from_env(self):
|
|
|
|
|
with patch.dict(os.environ, {
|
|
|
|
|
"HONCHO_API_KEY": "key",
|
|
|
|
|
"HONCHO_ENVIRONMENT": "staging",
|
|
|
|
|
}):
|
|
|
|
|
config = HonchoClientConfig.from_env()
|
|
|
|
|
assert config.environment == "staging"
|
|
|
|
|
|
|
|
|
|
def test_defaults_without_env(self):
|
|
|
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
|
|
|
# Remove HONCHO_API_KEY if it exists
|
|
|
|
|
os.environ.pop("HONCHO_API_KEY", None)
|
|
|
|
|
os.environ.pop("HONCHO_ENVIRONMENT", None)
|
|
|
|
|
config = HonchoClientConfig.from_env()
|
|
|
|
|
assert config.api_key is None
|
|
|
|
|
assert config.environment == "production"
|
|
|
|
|
|
|
|
|
|
def test_custom_workspace(self):
|
|
|
|
|
config = HonchoClientConfig.from_env(workspace_id="custom")
|
|
|
|
|
assert config.workspace_id == "custom"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFromGlobalConfig:
|
|
|
|
|
def test_missing_config_falls_back_to_env(self, tmp_path):
|
|
|
|
|
config = HonchoClientConfig.from_global_config(
|
|
|
|
|
config_path=tmp_path / "nonexistent.json"
|
|
|
|
|
)
|
|
|
|
|
# Should fall back to from_env
|
|
|
|
|
assert config.enabled is True or config.api_key is None # depends on env
|
|
|
|
|
|
|
|
|
|
def test_reads_full_config(self, tmp_path):
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({
|
|
|
|
|
"apiKey": "my-honcho-key",
|
|
|
|
|
"workspace": "my-workspace",
|
|
|
|
|
"environment": "staging",
|
|
|
|
|
"peerName": "alice",
|
|
|
|
|
"aiPeer": "hermes-custom",
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"saveMessages": False,
|
|
|
|
|
"contextTokens": 2000,
|
|
|
|
|
"sessionStrategy": "per-project",
|
|
|
|
|
"sessionPeerPrefix": True,
|
|
|
|
|
"sessions": {"/home/user/proj": "my-session"},
|
|
|
|
|
"hosts": {
|
|
|
|
|
"hermes": {
|
|
|
|
|
"workspace": "override-ws",
|
|
|
|
|
"aiPeer": "override-ai",
|
|
|
|
|
"linkedHosts": ["cursor"],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
assert config.api_key == "my-honcho-key"
|
|
|
|
|
# Host block workspace overrides root workspace
|
|
|
|
|
assert config.workspace_id == "override-ws"
|
|
|
|
|
assert config.ai_peer == "override-ai"
|
|
|
|
|
assert config.linked_hosts == ["cursor"]
|
|
|
|
|
assert config.environment == "staging"
|
|
|
|
|
assert config.peer_name == "alice"
|
|
|
|
|
assert config.enabled is True
|
|
|
|
|
assert config.save_messages is False
|
|
|
|
|
assert config.session_strategy == "per-project"
|
|
|
|
|
assert config.session_peer_prefix is True
|
|
|
|
|
|
|
|
|
|
def test_host_block_overrides_root(self, tmp_path):
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({
|
|
|
|
|
"apiKey": "key",
|
|
|
|
|
"workspace": "root-ws",
|
|
|
|
|
"aiPeer": "root-ai",
|
|
|
|
|
"hosts": {
|
|
|
|
|
"hermes": {
|
|
|
|
|
"workspace": "host-ws",
|
|
|
|
|
"aiPeer": "host-ai",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
assert config.workspace_id == "host-ws"
|
|
|
|
|
assert config.ai_peer == "host-ai"
|
|
|
|
|
|
|
|
|
|
def test_root_fields_used_when_no_host_block(self, tmp_path):
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({
|
|
|
|
|
"apiKey": "key",
|
|
|
|
|
"workspace": "root-ws",
|
|
|
|
|
"aiPeer": "root-ai",
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
assert config.workspace_id == "root-ws"
|
|
|
|
|
assert config.ai_peer == "root-ai"
|
|
|
|
|
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
def test_session_strategy_default_from_global_config(self, tmp_path):
|
|
|
|
|
"""from_global_config with no sessionStrategy should match dataclass default."""
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({"apiKey": "key"}))
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
2026-03-11 15:42:35 -04:00
|
|
|
assert config.session_strategy == "per-session"
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
|
|
|
|
def test_context_tokens_host_block_wins(self, tmp_path):
|
|
|
|
|
"""Host block contextTokens should override root."""
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({
|
|
|
|
|
"apiKey": "key",
|
|
|
|
|
"contextTokens": 1000,
|
|
|
|
|
"hosts": {"hermes": {"contextTokens": 2000}},
|
|
|
|
|
}))
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
assert config.context_tokens == 2000
|
|
|
|
|
|
|
|
|
|
def test_recall_mode_from_config(self, tmp_path):
|
|
|
|
|
"""recallMode is read from config, host block wins."""
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({
|
|
|
|
|
"apiKey": "key",
|
|
|
|
|
"recallMode": "tools",
|
|
|
|
|
"hosts": {"hermes": {"recallMode": "context"}},
|
|
|
|
|
}))
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
assert config.recall_mode == "context"
|
|
|
|
|
|
|
|
|
|
def test_recall_mode_default(self, tmp_path):
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({"apiKey": "key"}))
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
2026-03-09 17:46:51 -04:00
|
|
|
assert config.recall_mode == "hybrid"
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
def test_corrupt_config_falls_back_to_env(self, tmp_path):
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text("not valid json{{{")
|
|
|
|
|
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
# Should fall back to from_env without crashing
|
|
|
|
|
assert isinstance(config, HonchoClientConfig)
|
|
|
|
|
|
|
|
|
|
def test_api_key_env_fallback(self, tmp_path):
|
|
|
|
|
config_file = tmp_path / "config.json"
|
|
|
|
|
config_file.write_text(json.dumps({"enabled": True}))
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"HONCHO_API_KEY": "env-key"}):
|
|
|
|
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
|
|
|
|
assert config.api_key == "env-key"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestResolveSessionName:
|
|
|
|
|
def test_manual_override(self):
|
|
|
|
|
config = HonchoClientConfig(sessions={"/home/user/proj": "custom-session"})
|
|
|
|
|
assert config.resolve_session_name("/home/user/proj") == "custom-session"
|
|
|
|
|
|
|
|
|
|
def test_derive_from_dirname(self):
|
|
|
|
|
config = HonchoClientConfig()
|
|
|
|
|
result = config.resolve_session_name("/home/user/my-project")
|
|
|
|
|
assert result == "my-project"
|
|
|
|
|
|
|
|
|
|
def test_peer_prefix(self):
|
|
|
|
|
config = HonchoClientConfig(peer_name="alice", session_peer_prefix=True)
|
|
|
|
|
result = config.resolve_session_name("/home/user/proj")
|
|
|
|
|
assert result == "alice-proj"
|
|
|
|
|
|
|
|
|
|
def test_no_peer_prefix_when_no_peer_name(self):
|
|
|
|
|
config = HonchoClientConfig(session_peer_prefix=True)
|
|
|
|
|
result = config.resolve_session_name("/home/user/proj")
|
|
|
|
|
assert result == "proj"
|
|
|
|
|
|
|
|
|
|
def test_default_cwd(self):
|
|
|
|
|
config = HonchoClientConfig()
|
|
|
|
|
result = config.resolve_session_name()
|
|
|
|
|
# Should use os.getcwd() basename
|
|
|
|
|
assert result == Path.cwd().name
|
|
|
|
|
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
def test_per_repo_uses_git_root(self):
|
|
|
|
|
config = HonchoClientConfig(session_strategy="per-repo")
|
|
|
|
|
with patch.object(
|
|
|
|
|
HonchoClientConfig, "_git_repo_name", return_value="hermes-agent"
|
|
|
|
|
):
|
|
|
|
|
result = config.resolve_session_name("/home/user/hermes-agent/subdir")
|
|
|
|
|
assert result == "hermes-agent"
|
|
|
|
|
|
|
|
|
|
def test_per_repo_with_peer_prefix(self):
|
|
|
|
|
config = HonchoClientConfig(
|
|
|
|
|
session_strategy="per-repo", peer_name="eri", session_peer_prefix=True
|
|
|
|
|
)
|
|
|
|
|
with patch.object(
|
|
|
|
|
HonchoClientConfig, "_git_repo_name", return_value="groudon"
|
|
|
|
|
):
|
|
|
|
|
result = config.resolve_session_name("/home/user/groudon/src")
|
|
|
|
|
assert result == "eri-groudon"
|
|
|
|
|
|
|
|
|
|
def test_per_repo_falls_back_to_dirname_outside_git(self):
|
|
|
|
|
config = HonchoClientConfig(session_strategy="per-repo")
|
|
|
|
|
with patch.object(
|
|
|
|
|
HonchoClientConfig, "_git_repo_name", return_value=None
|
|
|
|
|
):
|
|
|
|
|
result = config.resolve_session_name("/home/user/not-a-repo")
|
|
|
|
|
assert result == "not-a-repo"
|
|
|
|
|
|
|
|
|
|
def test_per_repo_manual_override_still_wins(self):
|
|
|
|
|
config = HonchoClientConfig(
|
|
|
|
|
session_strategy="per-repo",
|
|
|
|
|
sessions={"/home/user/proj": "custom-session"},
|
|
|
|
|
)
|
|
|
|
|
result = config.resolve_session_name("/home/user/proj")
|
|
|
|
|
assert result == "custom-session"
|
|
|
|
|
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
class TestGetLinkedWorkspaces:
|
|
|
|
|
def test_resolves_linked_hosts(self):
|
|
|
|
|
config = HonchoClientConfig(
|
|
|
|
|
workspace_id="hermes-ws",
|
|
|
|
|
linked_hosts=["cursor", "windsurf"],
|
|
|
|
|
raw={
|
|
|
|
|
"hosts": {
|
|
|
|
|
"cursor": {"workspace": "cursor-ws"},
|
|
|
|
|
"windsurf": {"workspace": "windsurf-ws"},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
workspaces = config.get_linked_workspaces()
|
|
|
|
|
assert "cursor-ws" in workspaces
|
|
|
|
|
assert "windsurf-ws" in workspaces
|
|
|
|
|
|
|
|
|
|
def test_excludes_own_workspace(self):
|
|
|
|
|
config = HonchoClientConfig(
|
|
|
|
|
workspace_id="hermes-ws",
|
|
|
|
|
linked_hosts=["other"],
|
|
|
|
|
raw={"hosts": {"other": {"workspace": "hermes-ws"}}},
|
|
|
|
|
)
|
|
|
|
|
workspaces = config.get_linked_workspaces()
|
|
|
|
|
assert workspaces == []
|
|
|
|
|
|
|
|
|
|
def test_uses_host_key_as_fallback(self):
|
|
|
|
|
config = HonchoClientConfig(
|
|
|
|
|
workspace_id="hermes-ws",
|
|
|
|
|
linked_hosts=["cursor"],
|
|
|
|
|
raw={"hosts": {"cursor": {}}}, # no workspace field
|
|
|
|
|
)
|
|
|
|
|
workspaces = config.get_linked_workspaces()
|
|
|
|
|
assert "cursor" in workspaces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestResetHonchoClient:
|
|
|
|
|
def test_reset_clears_singleton(self):
|
|
|
|
|
import honcho_integration.client as mod
|
|
|
|
|
mod._honcho_client = MagicMock()
|
|
|
|
|
assert mod._honcho_client is not None
|
|
|
|
|
reset_honcho_client()
|
|
|
|
|
assert mod._honcho_client is None
|