| """Tests for plugins/memory/honcho/client.py — Honcho client configuration.""" |
|
|
| import importlib.util |
| import json |
| import os |
| from pathlib import Path |
| from unittest.mock import patch, MagicMock |
|
|
| import pytest |
|
|
| from plugins.memory.honcho.client import ( |
| HonchoClientConfig, |
| get_honcho_client, |
| reset_honcho_client, |
| resolve_active_host, |
| resolve_config_path, |
| 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.timeout is None |
| assert config.enabled is False |
| assert config.save_messages is True |
| assert config.session_strategy == "per-directory" |
| assert config.recall_mode == "hybrid" |
| assert config.session_peer_prefix is False |
| 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): |
| |
| 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" |
|
|
| def test_reads_base_url_from_env(self): |
| with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): |
| config = HonchoClientConfig.from_env() |
| assert config.base_url == "http://localhost:8000" |
| assert config.enabled is True |
|
|
| def test_enabled_without_api_key_when_base_url_set(self): |
| """base_url alone (no API key) is sufficient to enable a local instance.""" |
| with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): |
| os.environ.pop("HONCHO_API_KEY", None) |
| config = HonchoClientConfig.from_env() |
| assert config.api_key is None |
| assert config.base_url == "http://localhost:8000" |
| assert config.enabled is True |
|
|
| def test_reads_timeout_from_env(self): |
| with patch.dict(os.environ, {"HONCHO_TIMEOUT": "90"}, clear=True): |
| config = HonchoClientConfig.from_env() |
| assert config.timeout == 90.0 |
|
|
|
|
| class TestFromGlobalConfig: |
| def test_missing_config_falls_back_to_env(self, tmp_path): |
| with patch.dict(os.environ, {}, clear=True): |
| config = HonchoClientConfig.from_global_config( |
| config_path=tmp_path / "nonexistent.json" |
| ) |
| |
| assert config.enabled is False |
| assert config.api_key is None |
|
|
| def test_reads_full_config(self, tmp_path, monkeypatch): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "***", |
| "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", |
| } |
| } |
| })) |
| |
| monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated")) |
|
|
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.api_key == "***" |
| |
| assert config.workspace_id == "override-ws" |
| assert config.ai_peer == "override-ai" |
| 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" |
|
|
| 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": "***"})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.session_strategy == "per-directory" |
|
|
| def test_context_tokens_default_is_none(self, tmp_path): |
| """Default context_tokens should be None (uncapped) unless explicitly set.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***"})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.context_tokens is None |
|
|
| def test_context_tokens_explicit_sets_cap(self, tmp_path): |
| """Explicit contextTokens in config sets the cap.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***", "contextTokens": 1200})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.context_tokens == 1200 |
|
|
| def test_context_tokens_explicit_overrides_default(self, tmp_path): |
| """Explicit contextTokens in config should override the default.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***", "contextTokens": 2000})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.context_tokens == 2000 |
|
|
| 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) |
| assert config.recall_mode == "hybrid" |
|
|
| 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) |
| |
| 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" |
|
|
| def test_base_url_env_fallback(self, tmp_path): |
| """HONCHO_BASE_URL env var is used when no baseUrl in config JSON.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"workspace": "local"})) |
|
|
| with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.base_url == "http://localhost:8000" |
| assert config.enabled is True |
|
|
| def test_base_url_from_config_root(self, tmp_path): |
| """baseUrl in config root is read and takes precedence over env var.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"baseUrl": "http://config-host:9000"})) |
|
|
| with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.base_url == "http://config-host:9000" |
|
|
| def test_base_url_not_read_from_host_block(self, tmp_path): |
| """baseUrl is a root-level connection setting, not overridable per-host (consistent with apiKey).""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "baseUrl": "http://root:9000", |
| "hosts": {"hermes": {"baseUrl": "http://host-block:9001"}}, |
| })) |
|
|
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.base_url == "http://root:9000" |
|
|
| def test_timeout_from_config_root(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"timeout": 75})) |
|
|
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.timeout == 75.0 |
|
|
| def test_request_timeout_alias_from_config_root(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"requestTimeout": "82.5"})) |
|
|
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.timeout == 82.5 |
|
|
|
|
| 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() |
| |
| assert result == Path.cwd().name |
|
|
| 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" |
|
|
|
|
| class TestResolveConfigPath: |
| def test_prefers_hermes_home_when_exists(self, tmp_path): |
| hermes_home = tmp_path / "hermes" |
| hermes_home.mkdir() |
| local_cfg = hermes_home / "honcho.json" |
| local_cfg.write_text('{"apiKey": "local"}') |
|
|
| with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): |
| result = resolve_config_path() |
| assert result == local_cfg |
|
|
| def test_falls_back_to_global_when_no_local(self, tmp_path): |
| hermes_home = tmp_path / "hermes" |
| hermes_home.mkdir() |
| |
| |
| fake_home = tmp_path / "fakehome" |
| fake_home.mkdir() |
|
|
| with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), \ |
| patch.object(Path, "home", return_value=fake_home): |
| result = resolve_config_path() |
| assert result == GLOBAL_CONFIG_PATH |
|
|
| def test_falls_back_to_global_without_hermes_home_env(self, tmp_path): |
| fake_home = tmp_path / "fakehome" |
| fake_home.mkdir() |
|
|
| with patch.dict(os.environ, {}, clear=False), \ |
| patch.object(Path, "home", return_value=fake_home): |
| os.environ.pop("HERMES_HOME", None) |
| result = resolve_config_path() |
| assert result == GLOBAL_CONFIG_PATH |
|
|
| def test_from_global_config_uses_local_path(self, tmp_path): |
| hermes_home = tmp_path / "hermes" |
| hermes_home.mkdir() |
| local_cfg = hermes_home / "honcho.json" |
| local_cfg.write_text(json.dumps({ |
| "apiKey": "***", |
| "workspace": "local-ws", |
| })) |
|
|
| with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), \ |
| patch.object(Path, "home", return_value=tmp_path): |
| config = HonchoClientConfig.from_global_config() |
| assert config.api_key == "***" |
| assert config.workspace_id == "local-ws" |
|
|
|
|
| class TestResolveActiveHost: |
| def test_default_returns_hermes(self): |
| with patch.dict(os.environ, {}, clear=True): |
| os.environ.pop("HERMES_HONCHO_HOST", None) |
| os.environ.pop("HERMES_HOME", None) |
| assert resolve_active_host() == "hermes" |
|
|
| def test_explicit_env_var_wins(self): |
| with patch.dict(os.environ, {"HERMES_HONCHO_HOST": "hermes.coder"}): |
| assert resolve_active_host() == "hermes.coder" |
|
|
| def test_profile_name_derives_host(self): |
| with patch.dict(os.environ, {}, clear=False): |
| os.environ.pop("HERMES_HONCHO_HOST", None) |
| with patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"): |
| assert resolve_active_host() == "hermes.coder" |
|
|
| def test_default_profile_returns_hermes(self): |
| with patch.dict(os.environ, {}, clear=False): |
| os.environ.pop("HERMES_HONCHO_HOST", None) |
| with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"): |
| assert resolve_active_host() == "hermes" |
|
|
| def test_custom_profile_returns_hermes(self): |
| with patch.dict(os.environ, {}, clear=False): |
| os.environ.pop("HERMES_HONCHO_HOST", None) |
| with patch("hermes_cli.profiles.get_active_profile_name", return_value="custom"): |
| assert resolve_active_host() == "hermes" |
|
|
| def test_profiles_import_failure_falls_back(self): |
| import sys |
| with patch.dict(os.environ, {}, clear=False): |
| os.environ.pop("HERMES_HONCHO_HOST", None) |
| |
| saved = sys.modules.get("hermes_cli.profiles") |
| sys.modules["hermes_cli.profiles"] = None |
| try: |
| assert resolve_active_host() == "hermes" |
| finally: |
| if saved is not None: |
| sys.modules["hermes_cli.profiles"] = saved |
| else: |
| sys.modules.pop("hermes_cli.profiles", None) |
|
|
|
|
| class TestProfileScopedConfig: |
| def test_from_env_uses_profile_host(self): |
| with patch.dict(os.environ, {"HONCHO_API_KEY": "key"}): |
| config = HonchoClientConfig.from_env(host="hermes.coder") |
| assert config.host == "hermes.coder" |
| assert config.workspace_id == "hermes" |
| assert config.ai_peer == "hermes.coder" |
|
|
| def test_from_env_default_workspace_preserved_for_default_host(self): |
| with patch.dict(os.environ, {"HONCHO_API_KEY": "key"}): |
| config = HonchoClientConfig.from_env(host="hermes") |
| assert config.host == "hermes" |
| assert config.workspace_id == "hermes" |
|
|
| def test_from_global_config_reads_profile_host_block(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "shared-key", |
| "hosts": { |
| "hermes": {"aiPeer": "hermes", "peerName": "alice"}, |
| "hermes.coder": { |
| "aiPeer": "hermes.coder", |
| "peerName": "alice-coder", |
| "workspace": "coder-ws", |
| }, |
| }, |
| })) |
| config = HonchoClientConfig.from_global_config( |
| host="hermes.coder", config_path=config_file, |
| ) |
| assert config.host == "hermes.coder" |
| assert config.workspace_id == "coder-ws" |
| assert config.ai_peer == "hermes.coder" |
| assert config.peer_name == "alice-coder" |
|
|
| def test_from_global_config_auto_resolves_host(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "key", |
| "hosts": { |
| "hermes.dreamer": {"peerName": "dreamer-user"}, |
| }, |
| })) |
| with patch("plugins.memory.honcho.client.resolve_active_host", return_value="hermes.dreamer"): |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.host == "hermes.dreamer" |
| assert config.peer_name == "dreamer-user" |
|
|
|
|
| class TestObservationModeMigration: |
| """Existing configs without explicit observationMode keep 'unified' default.""" |
|
|
| def test_existing_config_defaults_to_unified(self, tmp_path): |
| """Config with host block but no observationMode → 'unified' (old default).""" |
| cfg_file = tmp_path / "config.json" |
| cfg_file.write_text(json.dumps({ |
| "apiKey": "k", |
| "hosts": {"hermes": {"enabled": True, "aiPeer": "hermes"}}, |
| })) |
| cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) |
| assert cfg.observation_mode == "unified" |
|
|
| def test_new_config_defaults_to_directional(self, tmp_path): |
| """Config with no host block and no credentials → 'directional' (new default).""" |
| cfg_file = tmp_path / "config.json" |
| cfg_file.write_text(json.dumps({})) |
| cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) |
| assert cfg.observation_mode == "directional" |
|
|
| def test_explicit_directional_respected(self, tmp_path): |
| """Existing config with explicit observationMode → uses what's set.""" |
| cfg_file = tmp_path / "config.json" |
| cfg_file.write_text(json.dumps({ |
| "apiKey": "k", |
| "hosts": {"hermes": {"enabled": True, "observationMode": "directional"}}, |
| })) |
| cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) |
| assert cfg.observation_mode == "directional" |
|
|
| def test_explicit_unified_respected(self, tmp_path): |
| """Existing config with explicit observationMode unified → stays unified.""" |
| cfg_file = tmp_path / "config.json" |
| cfg_file.write_text(json.dumps({ |
| "apiKey": "k", |
| "observationMode": "unified", |
| "hosts": {"hermes": {"enabled": True}}, |
| })) |
| cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) |
| assert cfg.observation_mode == "unified" |
|
|
| def test_granular_observation_overrides_preset(self, tmp_path): |
| """Explicit observation object overrides both preset and migration default.""" |
| cfg_file = tmp_path / "config.json" |
| cfg_file.write_text(json.dumps({ |
| "apiKey": "k", |
| "hosts": {"hermes": { |
| "enabled": True, |
| "observation": { |
| "user": {"observeMe": True, "observeOthers": False}, |
| "ai": {"observeMe": False, "observeOthers": True}, |
| }, |
| }}, |
| })) |
| cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) |
| |
| |
| assert cfg.user_observe_me is True |
| assert cfg.user_observe_others is False |
| assert cfg.ai_observe_me is False |
| assert cfg.ai_observe_others is True |
|
|
|
|
| class TestGetHonchoClient: |
| def teardown_method(self): |
| reset_honcho_client() |
|
|
| @pytest.mark.skipif( |
| not importlib.util.find_spec("honcho"), |
| reason="honcho SDK not installed" |
| ) |
| def test_passes_timeout_from_config(self): |
| fake_honcho = MagicMock(name="Honcho") |
| cfg = HonchoClientConfig( |
| api_key="test-key", |
| timeout=91.0, |
| workspace_id="hermes", |
| environment="production", |
| ) |
|
|
| with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho: |
| client = get_honcho_client(cfg) |
|
|
| assert client is fake_honcho |
| mock_honcho.assert_called_once() |
| assert mock_honcho.call_args.kwargs["timeout"] == 91.0 |
|
|
| @pytest.mark.skipif( |
| not importlib.util.find_spec("honcho"), |
| reason="honcho SDK not installed" |
| ) |
| def test_hermes_config_timeout_override_used_when_config_timeout_missing(self): |
| fake_honcho = MagicMock(name="Honcho") |
| cfg = HonchoClientConfig( |
| api_key="test-key", |
| workspace_id="hermes", |
| environment="production", |
| ) |
|
|
| with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \ |
| patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 88}}): |
| client = get_honcho_client(cfg) |
|
|
| assert client is fake_honcho |
| mock_honcho.assert_called_once() |
| assert mock_honcho.call_args.kwargs["timeout"] == 88.0 |
|
|
| @pytest.mark.skipif( |
| not importlib.util.find_spec("honcho"), |
| reason="honcho SDK not installed" |
| ) |
| def test_hermes_request_timeout_alias_used(self): |
| fake_honcho = MagicMock(name="Honcho") |
| cfg = HonchoClientConfig( |
| api_key="test-key", |
| workspace_id="hermes", |
| environment="production", |
| ) |
|
|
| with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \ |
| patch("hermes_cli.config.load_config", return_value={"honcho": {"request_timeout": "77.5"}}): |
| client = get_honcho_client(cfg) |
|
|
| assert client is fake_honcho |
| mock_honcho.assert_called_once() |
| assert mock_honcho.call_args.kwargs["timeout"] == 77.5 |
|
|
|
|
| class TestResolveSessionNameGatewayKey: |
| """Regression tests for gateway_session_key priority in resolve_session_name. |
| |
| Ensures gateway platforms get stable per-chat Honcho sessions even when |
| sessionStrategy=per-session would otherwise create ephemeral sessions. |
| Regression: plugin refactor 924bc67e dropped gateway key plumbing. |
| """ |
|
|
| def test_gateway_key_overrides_per_session_strategy(self): |
| """gateway_session_key must win over per-session session_id.""" |
| config = HonchoClientConfig(session_strategy="per-session") |
| result = config.resolve_session_name( |
| session_id="20260412_171002_69bb38", |
| gateway_session_key="agent:main:telegram:dm:8439114563", |
| ) |
| assert result == "agent-main-telegram-dm-8439114563" |
|
|
| def test_session_title_still_wins_over_gateway_key(self): |
| """Explicit /title remap takes priority over gateway_session_key.""" |
| config = HonchoClientConfig(session_strategy="per-session") |
| result = config.resolve_session_name( |
| session_title="my-custom-title", |
| session_id="20260412_171002_69bb38", |
| gateway_session_key="agent:main:telegram:dm:8439114563", |
| ) |
| assert result == "my-custom-title" |
|
|
| def test_per_session_fallback_without_gateway_key(self): |
| """Without gateway_session_key, per-session returns session_id (CLI path).""" |
| config = HonchoClientConfig(session_strategy="per-session") |
| result = config.resolve_session_name( |
| session_id="20260412_171002_69bb38", |
| gateway_session_key=None, |
| ) |
| assert result == "20260412_171002_69bb38" |
|
|
| def test_gateway_key_sanitizes_special_chars(self): |
| """Colons and other non-alphanumeric chars are replaced with hyphens.""" |
| config = HonchoClientConfig() |
| result = config.resolve_session_name( |
| gateway_session_key="agent:main:telegram:dm:8439114563", |
| ) |
| assert result == "agent-main-telegram-dm-8439114563" |
| assert ":" not in result |
|
|
|
|
| class TestResetHonchoClient: |
| def test_reset_clears_singleton(self): |
| import plugins.memory.honcho.client as mod |
| mod._honcho_client = MagicMock() |
| assert mod._honcho_client is not None |
| reset_honcho_client() |
| assert mod._honcho_client is None |
|
|
|
|
| class TestDialecticDepthParsing: |
| """Tests for _parse_dialectic_depth and _parse_dialectic_depth_levels.""" |
|
|
| def test_default_depth_is_1(self, tmp_path): |
| """Default dialecticDepth should be 1.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***"})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth == 1 |
|
|
| def test_depth_from_root(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***", "dialecticDepth": 2})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth == 2 |
|
|
| def test_depth_host_block_wins(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "***", |
| "dialecticDepth": 1, |
| "hosts": {"hermes": {"dialecticDepth": 3}}, |
| })) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth == 3 |
|
|
| def test_depth_clamped_high(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***", "dialecticDepth": 10})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth == 3 |
|
|
| def test_depth_clamped_low(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***", "dialecticDepth": -1})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth == 1 |
|
|
| def test_depth_levels_default_none(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({"apiKey": "***"})) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth_levels is None |
|
|
| def test_depth_levels_from_config(self, tmp_path): |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "***", |
| "dialecticDepth": 2, |
| "dialecticDepthLevels": ["minimal", "high"], |
| })) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth_levels == ["minimal", "high"] |
|
|
| def test_depth_levels_padded_if_short(self, tmp_path): |
| """Array shorter than depth gets padded with 'low'.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "***", |
| "dialecticDepth": 3, |
| "dialecticDepthLevels": ["high"], |
| })) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth_levels == ["high", "low", "low"] |
|
|
| def test_depth_levels_truncated_if_long(self, tmp_path): |
| """Array longer than depth gets truncated.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "***", |
| "dialecticDepth": 1, |
| "dialecticDepthLevels": ["high", "max", "medium"], |
| })) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth_levels == ["high"] |
|
|
| def test_depth_levels_invalid_values_default_to_low(self, tmp_path): |
| """Invalid reasoning levels in the array fall back to 'low'.""" |
| config_file = tmp_path / "config.json" |
| config_file.write_text(json.dumps({ |
| "apiKey": "***", |
| "dialecticDepth": 2, |
| "dialecticDepthLevels": ["invalid", "high"], |
| })) |
| config = HonchoClientConfig.from_global_config(config_path=config_file) |
| assert config.dialectic_depth_levels == ["low", "high"] |
|
|