diff --git a/tests/acp/test_server.py b/tests/acp/test_server.py index e3baee1c19..240392887d 100644 --- a/tests/acp/test_server.py +++ b/tests/acp/test_server.py @@ -167,13 +167,6 @@ class TestSessionOps: assert model_cmd.input is not None assert model_cmd.input.root.hint == "model name to switch to" - @pytest.mark.asyncio - async def test_new_session_schedules_available_commands_update(self, agent): - with patch.object(agent, "_schedule_available_commands_update") as mock_schedule: - resp = await agent.new_session(cwd="/home/user/project") - - mock_schedule.assert_called_once_with(resp.session_id) - @pytest.mark.asyncio async def test_cancel_sets_event(self, agent): resp = await agent.new_session(cwd=".") @@ -187,41 +180,11 @@ class TestSessionOps: # Should not raise await agent.cancel(session_id="does-not-exist") - @pytest.mark.asyncio - async def test_load_session_returns_response(self, agent): - resp = await agent.new_session(cwd="/tmp") - load_resp = await agent.load_session(cwd="/tmp", session_id=resp.session_id) - assert isinstance(load_resp, LoadSessionResponse) - - @pytest.mark.asyncio - async def test_load_session_schedules_available_commands_update(self, agent): - resp = await agent.new_session(cwd="/tmp") - with patch.object(agent, "_schedule_available_commands_update") as mock_schedule: - load_resp = await agent.load_session(cwd="/tmp", session_id=resp.session_id) - - assert isinstance(load_resp, LoadSessionResponse) - mock_schedule.assert_called_once_with(resp.session_id) - @pytest.mark.asyncio async def test_load_session_not_found_returns_none(self, agent): resp = await agent.load_session(cwd="/tmp", session_id="bogus") assert resp is None - @pytest.mark.asyncio - async def test_resume_session_returns_response(self, agent): - resp = await agent.new_session(cwd="/tmp") - resume_resp = await agent.resume_session(cwd="/tmp", session_id=resp.session_id) - assert isinstance(resume_resp, ResumeSessionResponse) - - @pytest.mark.asyncio - async def test_resume_session_schedules_available_commands_update(self, agent): - resp = await agent.new_session(cwd="/tmp") - with patch.object(agent, "_schedule_available_commands_update") as mock_schedule: - resume_resp = await agent.resume_session(cwd="/tmp", session_id=resp.session_id) - - assert isinstance(resume_resp, ResumeSessionResponse) - mock_schedule.assert_called_once_with(resp.session_id) - @pytest.mark.asyncio async def test_resume_session_creates_new_if_missing(self, agent): resume_resp = await agent.resume_session(cwd="/tmp", session_id="nonexistent") @@ -234,14 +197,6 @@ class TestSessionOps: class TestListAndFork: - @pytest.mark.asyncio - async def test_list_sessions(self, agent): - await agent.new_session(cwd="/a") - await agent.new_session(cwd="/b") - resp = await agent.list_sessions() - assert isinstance(resp, ListSessionsResponse) - assert len(resp.sessions) == 2 - @pytest.mark.asyncio async def test_fork_session(self, agent): new_resp = await agent.new_session(cwd="/original") @@ -249,16 +204,6 @@ class TestListAndFork: assert fork_resp.session_id assert fork_resp.session_id != new_resp.session_id - @pytest.mark.asyncio - async def test_fork_session_schedules_available_commands_update(self, agent): - new_resp = await agent.new_session(cwd="/original") - with patch.object(agent, "_schedule_available_commands_update") as mock_schedule: - fork_resp = await agent.fork_session(cwd="/forked", session_id=new_resp.session_id) - - assert fork_resp.session_id - mock_schedule.assert_called_once_with(fork_resp.session_id) - - # --------------------------------------------------------------------------- # session configuration / model routing # --------------------------------------------------------------------------- @@ -274,20 +219,6 @@ class TestSessionConfiguration: assert isinstance(resp, SetSessionModeResponse) assert getattr(state, "mode", None) == "chat" - @pytest.mark.asyncio - async def test_set_config_option_returns_response(self, agent): - new_resp = await agent.new_session(cwd="/tmp") - resp = await agent.set_config_option( - config_id="approval_mode", - session_id=new_resp.session_id, - value="auto", - ) - state = agent.session_manager.get_session(new_resp.session_id) - - assert isinstance(resp, SetSessionConfigOptionResponse) - assert getattr(state, "config_options", {}) == {"approval_mode": "auto"} - assert resp.config_options == [] - @pytest.mark.asyncio async def test_router_accepts_stable_session_config_methods(self, agent): new_resp = await agent.new_session(cwd="/tmp") @@ -808,47 +739,3 @@ class TestRegisterSessionMcpServers: with patch("tools.mcp_tool.register_mcp_servers", side_effect=RuntimeError("boom")): # Should not raise await agent._register_session_mcp_servers(state, [server]) - - @pytest.mark.asyncio - async def test_new_session_calls_register(self, agent, mock_manager): - """new_session passes mcp_servers to _register_session_mcp_servers.""" - with patch.object(agent, "_register_session_mcp_servers", new_callable=AsyncMock) as mock_reg: - resp = await agent.new_session(cwd="/tmp", mcp_servers=["fake"]) - assert resp is not None - mock_reg.assert_called_once() - # Second arg should be the mcp_servers list - assert mock_reg.call_args[0][1] == ["fake"] - - @pytest.mark.asyncio - async def test_load_session_calls_register(self, agent, mock_manager): - """load_session passes mcp_servers to _register_session_mcp_servers.""" - # Create a session first so load can find it - state = mock_manager.create_session(cwd="/tmp") - sid = state.session_id - - with patch.object(agent, "_register_session_mcp_servers", new_callable=AsyncMock) as mock_reg: - resp = await agent.load_session(cwd="/tmp", session_id=sid, mcp_servers=["fake"]) - assert resp is not None - mock_reg.assert_called_once() - - @pytest.mark.asyncio - async def test_resume_session_calls_register(self, agent, mock_manager): - """resume_session passes mcp_servers to _register_session_mcp_servers.""" - state = mock_manager.create_session(cwd="/tmp") - sid = state.session_id - - with patch.object(agent, "_register_session_mcp_servers", new_callable=AsyncMock) as mock_reg: - resp = await agent.resume_session(cwd="/tmp", session_id=sid, mcp_servers=["fake"]) - assert resp is not None - mock_reg.assert_called_once() - - @pytest.mark.asyncio - async def test_fork_session_calls_register(self, agent, mock_manager): - """fork_session passes mcp_servers to _register_session_mcp_servers.""" - state = mock_manager.create_session(cwd="/tmp") - sid = state.session_id - - with patch.object(agent, "_register_session_mcp_servers", new_callable=AsyncMock) as mock_reg: - resp = await agent.fork_session(cwd="/tmp", session_id=sid, mcp_servers=["fake"]) - assert resp is not None - mock_reg.assert_called_once() diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index 2cf64c33bf..5d79f96dea 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -436,17 +436,6 @@ class TestExpiredCodexFallback: class TestExplicitProviderRouting: """Test explicit provider selection bypasses auto chain correctly.""" - def test_explicit_anthropic_oauth(self, monkeypatch): - """provider='anthropic' + OAuth token should work with is_oauth=True.""" - monkeypatch.setenv("ANTHROPIC_TOKEN", "sk-ant-oat01-explicit-test") - with patch("agent.anthropic_adapter.build_anthropic_client") as mock_build: - mock_build.return_value = MagicMock() - client, model = resolve_provider_client("anthropic") - assert client is not None - # Verify OAuth flag propagated - adapter = client.chat.completions - assert adapter._is_oauth is True - def test_explicit_anthropic_api_key(self, monkeypatch): """provider='anthropic' + regular API key should work with is_oauth=False.""" with patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-api-regular-key"), \ @@ -458,146 +447,9 @@ class TestExplicitProviderRouting: adapter = client.chat.completions assert adapter._is_oauth is False - def test_explicit_openrouter(self, monkeypatch): - """provider='openrouter' should use OPENROUTER_API_KEY.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-explicit") - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - mock_openai.return_value = MagicMock() - client, model = resolve_provider_client("openrouter") - assert client is not None - - def test_explicit_kimi(self, monkeypatch): - """provider='kimi-coding' should use KIMI_API_KEY.""" - monkeypatch.setenv("KIMI_API_KEY", "kimi-test-key") - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - mock_openai.return_value = MagicMock() - client, model = resolve_provider_client("kimi-coding") - assert client is not None - - def test_explicit_minimax(self, monkeypatch): - """provider='minimax' should use MINIMAX_API_KEY.""" - monkeypatch.setenv("MINIMAX_API_KEY", "mm-test-key") - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - mock_openai.return_value = MagicMock() - client, model = resolve_provider_client("minimax") - assert client is not None - - def test_explicit_deepseek(self, monkeypatch): - """provider='deepseek' should use DEEPSEEK_API_KEY.""" - monkeypatch.setenv("DEEPSEEK_API_KEY", "ds-test-key") - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - mock_openai.return_value = MagicMock() - client, model = resolve_provider_client("deepseek") - assert client is not None - - def test_explicit_zai(self, monkeypatch): - """provider='zai' should use GLM_API_KEY.""" - monkeypatch.setenv("GLM_API_KEY", "zai-test-key") - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - mock_openai.return_value = MagicMock() - client, model = resolve_provider_client("zai") - assert client is not None - - def test_explicit_google_alias_uses_gemini_credentials(self): - """provider='google' should route through the gemini API-key provider.""" - with ( - patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={ - "api_key": "gemini-key", - "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", - }), - patch("agent.auxiliary_client.OpenAI") as mock_openai, - ): - mock_openai.return_value = MagicMock() - client, model = resolve_provider_client("google", model="gemini-3.1-pro-preview") - - assert client is not None - assert model == "gemini-3.1-pro-preview" - assert mock_openai.call_args.kwargs["api_key"] == "gemini-key" - assert mock_openai.call_args.kwargs["base_url"] == "https://generativelanguage.googleapis.com/v1beta/openai" - - def test_explicit_unknown_returns_none(self, monkeypatch): - """Unknown provider should return None.""" - client, model = resolve_provider_client("nonexistent-provider") - assert client is None - - class TestGetTextAuxiliaryClient: """Test the full resolution chain for get_text_auxiliary_client.""" - def test_openrouter_takes_priority(self, monkeypatch, codex_auth_dir): - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - client, model = get_text_auxiliary_client() - assert model == "google/gemini-3-flash-preview" - mock_openai.assert_called_once() - call_kwargs = mock_openai.call_args - assert call_kwargs.kwargs["api_key"] == "or-key" - - def test_nous_takes_priority_over_codex(self, monkeypatch, codex_auth_dir): - with patch("agent.auxiliary_client._read_nous_auth") as mock_nous, \ - patch("agent.auxiliary_client.OpenAI") as mock_openai: - mock_nous.return_value = {"access_token": "nous-tok"} - client, model = get_text_auxiliary_client() - assert model == "google/gemini-3-flash-preview" - - def test_custom_endpoint_over_codex(self, monkeypatch, codex_auth_dir): - config = { - "model": { - "provider": "custom", - "base_url": "http://localhost:1234/v1", - "default": "my-local-model", - } - } - monkeypatch.setenv("OPENAI_API_KEY", "lm-studio-key") - monkeypatch.setattr("hermes_cli.config.load_config", lambda: config) - monkeypatch.setattr("hermes_cli.runtime_provider.load_config", lambda: config) - # Override the autouse monkeypatch for codex - monkeypatch.setattr( - "agent.auxiliary_client._read_codex_access_token", - lambda: "codex-test-token-abc123", - ) - with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \ - patch("agent.auxiliary_client.OpenAI") as mock_openai: - client, model = get_text_auxiliary_client() - assert model == "my-local-model" - call_kwargs = mock_openai.call_args - assert call_kwargs.kwargs["base_url"] == "http://localhost:1234/v1" - - def test_custom_endpoint_uses_config_saved_base_url(self, monkeypatch): - config = { - "model": { - "provider": "custom", - "base_url": "http://localhost:1234/v1", - "default": "my-local-model", - } - } - monkeypatch.setenv("OPENAI_API_KEY", "lm-studio-key") - monkeypatch.setattr("hermes_cli.config.load_config", lambda: config) - monkeypatch.setattr("hermes_cli.runtime_provider.load_config", lambda: config) - - with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \ - patch("agent.auxiliary_client._read_codex_access_token", return_value=None), \ - patch("agent.auxiliary_client._resolve_api_key_provider", return_value=(None, None)), \ - patch("agent.auxiliary_client.OpenAI") as mock_openai: - client, model = get_text_auxiliary_client() - - assert client is not None - assert model == "my-local-model" - call_kwargs = mock_openai.call_args - assert call_kwargs.kwargs["base_url"] == "http://localhost:1234/v1" - - def test_codex_fallback_when_nothing_else(self, codex_auth_dir): - with patch("agent.auxiliary_client._try_openrouter", return_value=(None, None)), \ - patch("agent.auxiliary_client._try_nous", return_value=(None, None)), \ - patch("agent.auxiliary_client._try_custom_endpoint", return_value=(None, None)), \ - patch("agent.auxiliary_client._read_main_provider", return_value="openrouter"), \ - patch("agent.auxiliary_client.OpenAI") as mock_openai: - client, model = get_text_auxiliary_client() - assert model == "gpt-5.2-codex" - # Returns a CodexAuxiliaryClient wrapper, not a raw OpenAI client - from agent.auxiliary_client import CodexAuxiliaryClient - assert isinstance(client, CodexAuxiliaryClient) - def test_codex_pool_entry_takes_priority_over_auth_store(self): class _Entry: access_token = "pooled-codex-token" @@ -624,395 +476,6 @@ class TestGetTextAuxiliaryClient: assert isinstance(client, CodexAuxiliaryClient) assert model == "gpt-5.2-codex" - def test_returns_none_when_nothing_available(self, monkeypatch): - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) - with patch("agent.auxiliary_client._resolve_auto", return_value=(None, None)): - client, model = get_text_auxiliary_client() - assert client is None - assert model is None - - def test_custom_endpoint_uses_codex_wrapper_when_runtime_requests_responses_api(self, monkeypatch): - monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - with patch("agent.auxiliary_client._resolve_custom_runtime", - return_value=("https://api.openai.com/v1", "sk-test", "codex_responses")), \ - patch("agent.auxiliary_client._read_main_model", return_value="gpt-5.3-codex"), \ - patch("agent.auxiliary_client._try_openrouter", return_value=(None, None)), \ - patch("agent.auxiliary_client._try_nous", return_value=(None, None)), \ - patch("agent.auxiliary_client._read_main_provider", return_value="openrouter"), \ - patch("agent.auxiliary_client.OpenAI") as mock_openai: - client, model = get_text_auxiliary_client() - - from agent.auxiliary_client import CodexAuxiliaryClient - assert isinstance(client, CodexAuxiliaryClient) - assert model == "gpt-5.3-codex" - assert mock_openai.call_args.kwargs["base_url"] == "https://api.openai.com/v1" - assert mock_openai.call_args.kwargs["api_key"] == "sk-test" - - -class TestVisionClientFallback: - """Vision client auto mode resolves known-good multimodal backends.""" - - def test_vision_auto_includes_active_provider_when_configured(self, monkeypatch): - """Active provider appears in available backends when credentials exist.""" - monkeypatch.setenv("ANTHROPIC_API_KEY", "***") - with ( - patch("agent.auxiliary_client._read_nous_auth", return_value=None), - patch("agent.auxiliary_client._read_main_provider", return_value="anthropic"), - patch("agent.auxiliary_client._read_main_model", return_value="claude-sonnet-4"), - patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), - patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="***"), - ): - backends = get_available_vision_backends() - - assert "anthropic" in backends - - def test_resolve_provider_client_returns_native_anthropic_wrapper(self, monkeypatch): - monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-key") - with ( - patch("agent.auxiliary_client._read_nous_auth", return_value=None), - patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), - patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-api03-key"), - ): - client, model = resolve_provider_client("anthropic") - - assert client is not None - assert client.__class__.__name__ == "AnthropicAuxiliaryClient" - assert model == "claude-haiku-4-5-20251001" - - -class TestAuxiliaryPoolAwareness: - def test_try_nous_uses_pool_entry(self): - class _Entry: - access_token = "pooled-access-token" - agent_key = "pooled-agent-key" - inference_base_url = "https://inference.pool.example/v1" - - class _Pool: - def has_credentials(self): - return True - - def select(self): - return _Entry() - - with ( - patch("agent.auxiliary_client.load_pool", return_value=_Pool()), - patch("agent.auxiliary_client.OpenAI") as mock_openai, - ): - from agent.auxiliary_client import _try_nous - - client, model = _try_nous() - - assert client is not None - assert model == "gemini-3-flash" - call_kwargs = mock_openai.call_args.kwargs - assert call_kwargs["api_key"] == "pooled-agent-key" - assert call_kwargs["base_url"] == "https://inference.pool.example/v1" - - def test_resolve_provider_client_copilot_uses_runtime_credentials(self, monkeypatch): - monkeypatch.delenv("GITHUB_TOKEN", raising=False) - monkeypatch.delenv("GH_TOKEN", raising=False) - - with ( - patch( - "hermes_cli.auth.resolve_api_key_provider_credentials", - return_value={ - "provider": "copilot", - "api_key": "gh-cli-token", - "base_url": "https://api.githubcopilot.com", - "source": "gh auth token", - }, - ), - patch("agent.auxiliary_client.OpenAI") as mock_openai, - ): - client, model = resolve_provider_client("copilot", model="gpt-5.4") - - assert client is not None - assert model == "gpt-5.4" - call_kwargs = mock_openai.call_args.kwargs - assert call_kwargs["api_key"] == "gh-cli-token" - assert call_kwargs["base_url"] == "https://api.githubcopilot.com" - assert call_kwargs["default_headers"]["Editor-Version"] - - def test_copilot_responses_api_model_wrapped_in_codex_client(self, monkeypatch): - """Copilot GPT-5+ models (needing Responses API) are wrapped in CodexAuxiliaryClient.""" - monkeypatch.delenv("GITHUB_TOKEN", raising=False) - monkeypatch.delenv("GH_TOKEN", raising=False) - - with ( - patch( - "hermes_cli.auth.resolve_api_key_provider_credentials", - return_value={ - "provider": "copilot", - "api_key": "test-token", - "base_url": "https://api.githubcopilot.com", - "source": "gh auth token", - }, - ), - patch("agent.auxiliary_client.OpenAI"), - ): - client, model = resolve_provider_client("copilot", model="gpt-5.4-mini") - - from agent.auxiliary_client import CodexAuxiliaryClient - assert isinstance(client, CodexAuxiliaryClient) - assert model == "gpt-5.4-mini" - - def test_copilot_chat_completions_model_not_wrapped(self, monkeypatch): - """Copilot models using Chat Completions are returned as plain OpenAI clients.""" - monkeypatch.delenv("GITHUB_TOKEN", raising=False) - monkeypatch.delenv("GH_TOKEN", raising=False) - - with ( - patch( - "hermes_cli.auth.resolve_api_key_provider_credentials", - return_value={ - "provider": "copilot", - "api_key": "test-token", - "base_url": "https://api.githubcopilot.com", - "source": "gh auth token", - }, - ), - patch("agent.auxiliary_client.OpenAI") as mock_openai, - ): - client, model = resolve_provider_client("copilot", model="gpt-4.1-mini") - - from agent.auxiliary_client import CodexAuxiliaryClient - assert not isinstance(client, CodexAuxiliaryClient) - assert model == "gpt-4.1-mini" - # Should be the raw mock OpenAI client - assert client is mock_openai.return_value - - def test_vision_auto_uses_active_provider_as_fallback(self, monkeypatch): - """When no OpenRouter/Nous available, vision auto falls back to active provider.""" - monkeypatch.setenv("ANTHROPIC_API_KEY", "***") - with ( - patch("agent.auxiliary_client._read_nous_auth", return_value=None), - patch("agent.auxiliary_client._read_main_provider", return_value="anthropic"), - patch("agent.auxiliary_client._read_main_model", return_value="claude-sonnet-4"), - patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), - patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="***"), - ): - provider, client, model = resolve_vision_provider_client() - - assert client is not None - assert client.__class__.__name__ == "AnthropicAuxiliaryClient" - - def test_vision_auto_prefers_active_provider_over_openrouter(self, monkeypatch): - """Active provider is tried before OpenRouter in vision auto.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - monkeypatch.setenv("ANTHROPIC_API_KEY", "***") - - with ( - patch("agent.auxiliary_client._read_nous_auth", return_value=None), - patch("agent.auxiliary_client._read_main_provider", return_value="anthropic"), - patch("agent.auxiliary_client._read_main_model", return_value="claude-sonnet-4"), - patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), - patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="***"), - ): - provider, client, model = resolve_vision_provider_client() - - # Active provider should win over OpenRouter - assert provider == "anthropic" - - def test_vision_auto_uses_named_custom_as_active_provider(self, monkeypatch): - """Named custom provider works as active provider fallback in vision auto.""" - monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) - monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) - with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \ - patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)), \ - patch("agent.auxiliary_client._read_main_provider", return_value="custom:local"), \ - patch("agent.auxiliary_client._read_main_model", return_value="my-local-model"), \ - patch("agent.auxiliary_client.resolve_provider_client", - return_value=(MagicMock(), "my-local-model")) as mock_resolve: - provider, client, model = resolve_vision_provider_client() - assert client is not None - assert provider == "custom:local" - - def test_vision_config_google_provider_uses_gemini_credentials(self, monkeypatch): - config = { - "auxiliary": { - "vision": { - "provider": "google", - "model": "gemini-3.1-pro-preview", - } - } - } - monkeypatch.setattr("hermes_cli.config.load_config", lambda: config) - with ( - patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={ - "api_key": "gemini-key", - "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", - }), - patch("agent.auxiliary_client.OpenAI") as mock_openai, - ): - resolved_provider, client, model = resolve_vision_provider_client() - - assert resolved_provider == "gemini" - assert client is not None - assert model == "gemini-3.1-pro-preview" - assert mock_openai.call_args.kwargs["api_key"] == "gemini-key" - assert mock_openai.call_args.kwargs["base_url"] == "https://generativelanguage.googleapis.com/v1beta/openai" - - - -class TestTaskSpecificOverrides: - """Integration tests for per-task provider routing via get_text_auxiliary_client(task=...).""" - - def test_task_direct_endpoint_from_config(self, monkeypatch, tmp_path): - hermes_home = tmp_path / "hermes" - hermes_home.mkdir(parents=True, exist_ok=True) - (hermes_home / "config.yaml").write_text( - """auxiliary: - web_extract: - base_url: http://localhost:3456/v1 - api_key: config-key - model: config-model -""" - ) - monkeypatch.setenv("HERMES_HOME", str(hermes_home)) - with patch("agent.auxiliary_client.OpenAI") as mock_openai: - client, model = get_text_auxiliary_client("web_extract") - assert model == "config-model" - assert mock_openai.call_args.kwargs["base_url"] == "http://localhost:3456/v1" - assert mock_openai.call_args.kwargs["api_key"] == "config-key" - - def test_task_without_override_uses_auto(self, monkeypatch): - """A task with no provider env var falls through to auto chain.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - with patch("agent.auxiliary_client.OpenAI"): - client, model = get_text_auxiliary_client("compression") - assert model == "google/gemini-3-flash-preview" # auto → OpenRouter - - def test_resolve_auto_prefers_live_main_runtime_over_persisted_config(self, monkeypatch, tmp_path): - """Session-only live model switches should override persisted config for auto routing.""" - hermes_home = tmp_path / "hermes" - hermes_home.mkdir(parents=True, exist_ok=True) - (hermes_home / "config.yaml").write_text( - """model: - default: glm-5.1 - provider: opencode-go -""" - ) - monkeypatch.setenv("HERMES_HOME", str(hermes_home)) - - calls = [] - - def _fake_resolve(provider, model=None, *args, **kwargs): - calls.append((provider, model, kwargs)) - return MagicMock(), model or "resolved-model" - - with patch("agent.auxiliary_client.resolve_provider_client", side_effect=_fake_resolve): - client, model = _resolve_auto( - main_runtime={ - "provider": "openai-codex", - "model": "gpt-5.4", - "api_mode": "codex_responses", - } - ) - - assert client is not None - assert model == "gpt-5.4" - assert calls[0][0] == "openai-codex" - assert calls[0][1] == "gpt-5.4" - assert calls[0][2]["api_mode"] == "codex_responses" - - def test_explicit_compression_pin_still_wins_over_live_main_runtime(self, monkeypatch, tmp_path): - """Task-level compression config should beat a live session override.""" - hermes_home = tmp_path / "hermes" - hermes_home.mkdir(parents=True, exist_ok=True) - (hermes_home / "config.yaml").write_text( - """auxiliary: - compression: - provider: openrouter - model: google/gemini-3-flash-preview -model: - default: glm-5.1 - provider: opencode-go -""" - ) - monkeypatch.setenv("HERMES_HOME", str(hermes_home)) - - with patch("agent.auxiliary_client.resolve_provider_client", return_value=(MagicMock(), "google/gemini-3-flash-preview")) as mock_resolve: - client, model = get_text_auxiliary_client( - "compression", - main_runtime={ - "provider": "openai-codex", - "model": "gpt-5.4", - }, - ) - - assert client is not None - assert model == "google/gemini-3-flash-preview" - assert mock_resolve.call_args.args[0] == "openrouter" - assert mock_resolve.call_args.kwargs["main_runtime"] == { - "provider": "openai-codex", - "model": "gpt-5.4", - } - - -def test_resolve_provider_client_supports_copilot_acp_external_process(): - fake_client = MagicMock() - - with patch("agent.auxiliary_client._read_main_model", return_value="gpt-5.4-mini"), \ - patch("agent.auxiliary_client.CodexAuxiliaryClient", MagicMock()), \ - patch("agent.copilot_acp_client.CopilotACPClient", return_value=fake_client) as mock_acp, \ - patch("hermes_cli.auth.resolve_external_process_provider_credentials", return_value={ - "provider": "copilot-acp", - "api_key": "copilot-acp", - "base_url": "acp://copilot", - "command": "/usr/bin/copilot", - "args": ["--acp", "--stdio"], - }): - client, model = resolve_provider_client("copilot-acp") - - assert client is fake_client - assert model == "gpt-5.4-mini" - assert mock_acp.call_args.kwargs["api_key"] == "copilot-acp" - assert mock_acp.call_args.kwargs["base_url"] == "acp://copilot" - assert mock_acp.call_args.kwargs["command"] == "/usr/bin/copilot" - assert mock_acp.call_args.kwargs["args"] == ["--acp", "--stdio"] - - -def test_resolve_provider_client_copilot_acp_requires_explicit_or_configured_model(): - with patch("agent.auxiliary_client._read_main_model", return_value=""), \ - patch("agent.copilot_acp_client.CopilotACPClient") as mock_acp, \ - patch("hermes_cli.auth.resolve_external_process_provider_credentials", return_value={ - "provider": "copilot-acp", - "api_key": "copilot-acp", - "base_url": "acp://copilot", - "command": "/usr/bin/copilot", - "args": ["--acp", "--stdio"], - }): - client, model = resolve_provider_client("copilot-acp") - - assert client is None - assert model is None - mock_acp.assert_not_called() - - -class TestAuxiliaryMaxTokensParam: - def test_codex_fallback_uses_max_tokens(self, monkeypatch): - """Codex adapter translates max_tokens internally, so we return max_tokens.""" - with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \ - patch("agent.auxiliary_client._read_codex_access_token", return_value="tok"): - result = auxiliary_max_tokens_param(1024) - assert result == {"max_tokens": 1024} - - def test_openrouter_uses_max_tokens(self, monkeypatch): - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - result = auxiliary_max_tokens_param(1024) - assert result == {"max_tokens": 1024} - - def test_no_provider_uses_max_tokens(self): - with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \ - patch("agent.auxiliary_client._read_codex_access_token", return_value=None): - result = auxiliary_max_tokens_param(1024) - assert result == {"max_tokens": 1024} - - # ── Payment / credit exhaustion fallback ───────────────────────────────── @@ -1126,83 +589,6 @@ class TestCallLlmPaymentFallback: exc.status_code = 402 return exc - def test_402_triggers_fallback_when_auto(self, monkeypatch): - """When provider is auto and returns 402, call_llm tries the next one.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - primary_client.chat.completions.create.side_effect = self._make_402_error() - - fallback_client = MagicMock() - fallback_response = MagicMock() - fallback_client.chat.completions.create.return_value = fallback_response - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "google/gemini-3-flash-preview")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("auto", "google/gemini-3-flash-preview", None, None, None)), \ - patch("agent.auxiliary_client._try_payment_fallback", - return_value=(fallback_client, "gpt-5.2-codex", "openai-codex")) as mock_fb: - result = call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - assert result is fallback_response - mock_fb.assert_called_once_with("auto", "compression", reason="payment error") - # Fallback call should use the fallback model - fb_kwargs = fallback_client.chat.completions.create.call_args.kwargs - assert fb_kwargs["model"] == "gpt-5.2-codex" - - def test_402_no_fallback_when_explicit_provider(self, monkeypatch): - """When provider is explicitly configured (not auto), 402 should NOT fallback (#7559).""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - primary_client.chat.completions.create.side_effect = self._make_402_error() - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "local-model")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("custom", "local-model", None, None, None)), \ - patch("agent.auxiliary_client._try_payment_fallback") as mock_fb: - with pytest.raises(Exception, match="insufficient credits"): - call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - # Fallback should NOT be attempted when provider is explicit - mock_fb.assert_not_called() - - def test_connection_error_triggers_fallback_when_auto(self, monkeypatch): - """Connection errors also trigger fallback when provider is auto.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - conn_err = Exception("Connection refused") - conn_err.status_code = None - primary_client.chat.completions.create.side_effect = conn_err - - fallback_client = MagicMock() - fallback_response = MagicMock() - fallback_client.chat.completions.create.return_value = fallback_response - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "model")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("auto", "model", None, None, None)), \ - patch("agent.auxiliary_client._is_connection_error", return_value=True), \ - patch("agent.auxiliary_client._try_payment_fallback", - return_value=(fallback_client, "fb-model", "nous")) as mock_fb: - result = call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - assert result is fallback_response - mock_fb.assert_called_once_with("auto", "compression", reason="connection error") - def test_non_payment_error_not_caught(self, monkeypatch): """Non-payment/non-connection errors (500) should NOT trigger fallback.""" monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") @@ -1222,26 +608,6 @@ class TestCallLlmPaymentFallback: messages=[{"role": "user", "content": "hello"}], ) - def test_402_with_no_fallback_reraises(self, monkeypatch): - """When 402 hits and no fallback is available, the original error propagates.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - primary_client.chat.completions.create.side_effect = self._make_402_error() - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "google/gemini-3-flash-preview")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("auto", "google/gemini-3-flash-preview", None, None, None)), \ - patch("agent.auxiliary_client._try_payment_fallback", - return_value=(None, None, "")): - with pytest.raises(Exception, match="insufficient credits"): - call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - # --------------------------------------------------------------------------- # Gate: _resolve_api_key_provider must skip anthropic when not configured # --------------------------------------------------------------------------- @@ -1289,59 +655,11 @@ def test_resolve_api_key_provider_skips_unconfigured_anthropic(monkeypatch): # --------------------------------------------------------------------------- -class TestModelDefaultElimination: - """_resolve_api_key_provider must skip providers without known aux models.""" - - def test_unknown_provider_skipped(self, monkeypatch): - """Providers not in _API_KEY_PROVIDER_AUX_MODELS are skipped, not sent model='default'.""" - from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS - - # Verify our known providers have entries - assert "gemini" in _API_KEY_PROVIDER_AUX_MODELS - assert "kimi-coding" in _API_KEY_PROVIDER_AUX_MODELS - - # A random provider_id not in the dict should return None - assert _API_KEY_PROVIDER_AUX_MODELS.get("totally-unknown-provider") is None - - def test_known_provider_gets_real_model(self): - """Known providers get a real model name, not 'default'.""" - from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS - - for provider_id, model in _API_KEY_PROVIDER_AUX_MODELS.items(): - assert model != "default", f"{provider_id} should not map to 'default'" - assert isinstance(model, str) and model.strip(), \ - f"{provider_id} should have a non-empty model string" - - # --------------------------------------------------------------------------- # _try_payment_fallback reason parameter (#7512 bug 3) # --------------------------------------------------------------------------- -class TestTryPaymentFallbackReason: - """_try_payment_fallback uses the reason parameter in log messages.""" - - def test_reason_parameter_passed_through(self, monkeypatch): - """The reason= parameter is accepted without error.""" - from agent.auxiliary_client import _try_payment_fallback - - # Mock the provider chain to return nothing - monkeypatch.setattr( - "agent.auxiliary_client._get_provider_chain", - lambda: [], - ) - monkeypatch.setattr( - "agent.auxiliary_client._read_main_provider", - lambda: "", - ) - - client, model, label = _try_payment_fallback( - "openrouter", task="compression", reason="connection error" - ) - assert client is None - assert label == "" - - # --------------------------------------------------------------------------- # _is_connection_error coverage # --------------------------------------------------------------------------- @@ -1383,98 +701,6 @@ class TestIsConnectionError: # --------------------------------------------------------------------------- -class TestAsyncCallLlmFallback: - """async_call_llm mirrors call_llm fallback behavior.""" - - def _make_402_error(self, msg="Payment Required: insufficient credits"): - exc = Exception(msg) - exc.status_code = 402 - return exc - - @pytest.mark.asyncio - async def test_402_triggers_async_fallback_when_auto(self, monkeypatch): - """When provider is auto and returns 402, async_call_llm tries fallback.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - primary_client.chat.completions.create = AsyncMock( - side_effect=self._make_402_error()) - - # Fallback client (sync) returned by _try_payment_fallback - fb_sync_client = MagicMock() - fb_async_client = MagicMock() - fb_response = MagicMock() - fb_async_client.chat.completions.create = AsyncMock(return_value=fb_response) - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "google/gemini-3-flash-preview")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("auto", "google/gemini-3-flash-preview", None, None, None)), \ - patch("agent.auxiliary_client._try_payment_fallback", - return_value=(fb_sync_client, "gpt-5.2-codex", "openai-codex")) as mock_fb, \ - patch("agent.auxiliary_client._to_async_client", - return_value=(fb_async_client, "gpt-5.2-codex")): - result = await async_call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - assert result is fb_response - mock_fb.assert_called_once_with("auto", "compression", reason="payment error") - - @pytest.mark.asyncio - async def test_402_no_async_fallback_when_explicit(self, monkeypatch): - """When provider is explicit, 402 should NOT trigger async fallback.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - primary_client.chat.completions.create = AsyncMock( - side_effect=self._make_402_error()) - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "local-model")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("custom", "local-model", None, None, None)), \ - patch("agent.auxiliary_client._try_payment_fallback") as mock_fb: - with pytest.raises(Exception, match="insufficient credits"): - await async_call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - mock_fb.assert_not_called() - - @pytest.mark.asyncio - async def test_connection_error_triggers_async_fallback(self, monkeypatch): - """Connection errors trigger async fallback when provider is auto.""" - monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") - - primary_client = MagicMock() - conn_err = Exception("Connection refused") - conn_err.status_code = None - primary_client.chat.completions.create = AsyncMock(side_effect=conn_err) - - fb_sync_client = MagicMock() - fb_async_client = MagicMock() - fb_response = MagicMock() - fb_async_client.chat.completions.create = AsyncMock(return_value=fb_response) - - with patch("agent.auxiliary_client._get_cached_client", - return_value=(primary_client, "model")), \ - patch("agent.auxiliary_client._resolve_task_provider_model", - return_value=("auto", "model", None, None, None)), \ - patch("agent.auxiliary_client._is_connection_error", return_value=True), \ - patch("agent.auxiliary_client._try_payment_fallback", - return_value=(fb_sync_client, "fb-model", "nous")) as mock_fb, \ - patch("agent.auxiliary_client._to_async_client", - return_value=(fb_async_client, "fb-model")): - result = await async_call_llm( - task="compression", - messages=[{"role": "user", "content": "hello"}], - ) - - assert result is fb_response - mock_fb.assert_called_once_with("auto", "compression", reason="connection error") class TestStaleBaseUrlWarning: """_resolve_auto() warns when OPENAI_BASE_URL conflicts with config provider (#5161).""" @@ -1546,24 +772,6 @@ class TestStaleBaseUrlWarning: assert not any("OPENAI_BASE_URL is set" in rec.message for rec in caplog.records), \ "Should NOT warn when OPENAI_BASE_URL is not set" - def test_warning_only_fires_once(self, monkeypatch, caplog): - """Warning is suppressed after the first invocation.""" - import agent.auxiliary_client as mod - monkeypatch.setattr(mod, "_stale_base_url_warned", False) - monkeypatch.setenv("OPENAI_BASE_URL", "http://localhost:11434/v1") - monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test") - - with patch("agent.auxiliary_client._read_main_provider", return_value="openrouter"), \ - patch("agent.auxiliary_client._read_main_model", return_value="google/gemini-flash"), \ - caplog.at_level(logging.WARNING, logger="agent.auxiliary_client"): - _resolve_auto() - caplog.clear() - _resolve_auto() - - assert not any("OPENAI_BASE_URL is set" in rec.message for rec in caplog.records), \ - "Warning should not fire a second time" - - # --------------------------------------------------------------------------- # Anthropic-compatible image block conversion # --------------------------------------------------------------------------- diff --git a/tests/agent/test_gemini_cloudcode.py b/tests/agent/test_gemini_cloudcode.py index 8a3bb99a9b..cf5e80f08a 100644 --- a/tests/agent/test_gemini_cloudcode.py +++ b/tests/agent/test_gemini_cloudcode.py @@ -826,85 +826,6 @@ class TestGeminiCloudCodeClient: finally: client.close() - def test_create_with_mocked_http(self, monkeypatch): - """End-to-end: mock oauth + http, verify translation works.""" - from agent import gemini_cloudcode_adapter, google_oauth - from agent.google_oauth import GoogleCredentials, save_credentials - - # Set up logged-in state - save_credentials(GoogleCredentials( - access_token="bearer-tok", - refresh_token="rt", - expires_ms=int((time.time() + 3600) * 1000), - project_id="test-proj", - )) - - # Mock the HTTP response - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = { - "response": { - "candidates": [{ - "content": {"parts": [{"text": "hello from mock"}]}, - "finishReason": "STOP", - }], - "usageMetadata": { - "promptTokenCount": 5, - "candidatesTokenCount": 3, - "totalTokenCount": 8, - }, - } - } - - client = gemini_cloudcode_adapter.GeminiCloudCodeClient() - try: - with patch.object(client._http, "post", return_value=mock_response) as mock_post: - result = client.chat.completions.create( - model="gemini-2.5-flash", - messages=[{"role": "user", "content": "hi"}], - ) - assert result.choices[0].message.content == "hello from mock" - - # Verify the request was wrapped correctly - call_args = mock_post.call_args - assert "cloudcode-pa.googleapis.com" in call_args[0][0] - assert ":generateContent" in call_args[0][0] - json_body = call_args[1]["json"] - assert json_body["project"] == "test-proj" - assert json_body["model"] == "gemini-2.5-flash" - assert "request" in json_body - # Auth header - assert call_args[1]["headers"]["Authorization"] == "Bearer bearer-tok" - finally: - client.close() - - def test_create_raises_on_http_error(self, monkeypatch): - from agent import gemini_cloudcode_adapter - from agent.google_oauth import GoogleCredentials, save_credentials - - save_credentials(GoogleCredentials( - access_token="tok", refresh_token="rt", - expires_ms=int((time.time() + 3600) * 1000), - project_id="p", - )) - - mock_response = MagicMock() - mock_response.status_code = 401 - mock_response.text = "unauthorized" - - client = gemini_cloudcode_adapter.GeminiCloudCodeClient() - try: - with patch.object(client._http, "post", return_value=mock_response): - with pytest.raises(gemini_cloudcode_adapter.CodeAssistError) as exc_info: - client.chat.completions.create( - model="gemini-2.5-flash", - messages=[{"role": "user", "content": "hi"}], - ) - assert exc_info.value.code == "code_assist_unauthorized" - finally: - client.close() - - # ============================================================================= # Provider registration # ============================================================================= @@ -916,14 +837,6 @@ class TestProviderRegistration: assert "google-gemini-cli" in PROVIDER_REGISTRY assert PROVIDER_REGISTRY["google-gemini-cli"].auth_type == "oauth_external" - @pytest.mark.parametrize("alias", [ - "gemini-cli", "gemini-oauth", "google-gemini-cli", - ]) - def test_alias_resolves(self, alias): - from hermes_cli.auth import resolve_provider - - assert resolve_provider(alias) == "google-gemini-cli" - def test_google_gemini_alias_still_goes_to_api_key_gemini(self): """Regression guard: don't shadow the existing google-gemini → gemini alias.""" from hermes_cli.auth import resolve_provider diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index 160b55efc6..e558afabeb 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -548,41 +548,6 @@ class TestDeliverResultWrapping: class TestDeliverResultErrorReturns: """Verify _deliver_result returns error strings on failure, None on success.""" - def test_returns_none_on_successful_delivery(self): - from gateway.config import Platform - - pconfig = MagicMock() - pconfig.enabled = True - mock_cfg = MagicMock() - mock_cfg.platforms = {Platform.TELEGRAM: pconfig} - - with patch("gateway.config.load_gateway_config", return_value=mock_cfg), \ - patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"success": True})): - job = { - "id": "ok-job", - "deliver": "origin", - "origin": {"platform": "telegram", "chat_id": "123"}, - } - result = _deliver_result(job, "Output.") - assert result is None - - def test_returns_none_for_local_delivery(self): - """local-only jobs don't deliver — not a failure.""" - job = {"id": "local-job", "deliver": "local"} - result = _deliver_result(job, "Output.") - assert result is None - - def test_returns_error_for_unknown_platform(self): - job = { - "id": "bad-platform", - "deliver": "origin", - "origin": {"platform": "fax", "chat_id": "123"}, - } - with patch("gateway.config.load_gateway_config"): - result = _deliver_result(job, "Output.") - assert result is not None - assert "unknown platform" in result - def test_returns_error_when_platform_disabled(self): from gateway.config import Platform @@ -601,25 +566,6 @@ class TestDeliverResultErrorReturns: assert result is not None assert "not configured" in result - def test_returns_error_on_send_failure(self): - from gateway.config import Platform - - pconfig = MagicMock() - pconfig.enabled = True - mock_cfg = MagicMock() - mock_cfg.platforms = {Platform.TELEGRAM: pconfig} - - with patch("gateway.config.load_gateway_config", return_value=mock_cfg), \ - patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"error": "rate limited"})): - job = { - "id": "rate-limited", - "deliver": "origin", - "origin": {"platform": "telegram", "chat_id": "123"}, - } - result = _deliver_result(job, "Output.") - assert result is not None - assert "rate limited" in result - def test_returns_error_for_unresolved_target(self, monkeypatch): """Non-local delivery with no resolvable target should return an error.""" monkeypatch.delenv("TELEGRAM_HOME_CHANNEL", raising=False) @@ -864,57 +810,6 @@ class TestRunJobConfigLogging: f"Expected 'failed to parse prefill messages' warning in logs, got: {[r.message for r in caplog.records]}" -class TestRunJobPerJobOverrides: - def test_job_level_model_provider_and_base_url_overrides_are_used(self, tmp_path): - config_yaml = tmp_path / "config.yaml" - config_yaml.write_text( - "model:\n" - " default: gpt-5.4\n" - " provider: openai-codex\n" - " base_url: https://chatgpt.com/backend-api/codex\n" - ) - - job = { - "id": "briefing-job", - "name": "briefing", - "prompt": "hello", - "model": "perplexity/sonar-pro", - "provider": "custom", - "base_url": "http://127.0.0.1:4000/v1", - } - - fake_db = MagicMock() - fake_runtime = { - "provider": "openrouter", - "api_mode": "chat_completions", - "base_url": "http://127.0.0.1:4000/v1", - "api_key": "***", - } - - with patch("cron.scheduler._hermes_home", tmp_path), \ - patch("cron.scheduler._resolve_origin", return_value=None), \ - patch("dotenv.load_dotenv"), \ - patch("hermes_state.SessionDB", return_value=fake_db), \ - patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value=fake_runtime) as runtime_mock, \ - patch("run_agent.AIAgent") as mock_agent_cls: - mock_agent = MagicMock() - mock_agent.run_conversation.return_value = {"final_response": "ok"} - mock_agent_cls.return_value = mock_agent - - success, output, final_response, error = run_job(job) - - assert success is True - assert error is None - assert final_response == "ok" - assert "ok" in output - runtime_mock.assert_called_once_with( - requested="custom", - explicit_base_url="http://127.0.0.1:4000/v1", - ) - assert mock_agent_cls.call_args.kwargs["model"] == "perplexity/sonar-pro" - fake_db.close.assert_called_once() - - class TestRunJobSkillBacked: def test_run_job_preserves_skill_env_passthrough_into_worker_thread(self, tmp_path): job = { @@ -1128,16 +1023,6 @@ class TestSilentDelivery: "origin": {"platform": "telegram", "chat_id": "123"}, } - def test_normal_response_delivers(self): - with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \ - patch("cron.scheduler.run_job", return_value=(True, "# output", "Results here", None)), \ - patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \ - patch("cron.scheduler._deliver_result") as deliver_mock, \ - patch("cron.scheduler.mark_job_run"): - from cron.scheduler import tick - tick(verbose=False) - deliver_mock.assert_called_once() - def test_silent_response_suppresses_delivery(self, caplog): with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \ patch("cron.scheduler.run_job", return_value=(True, "# output", "[SILENT]", None)), \ @@ -1277,44 +1162,6 @@ class TestBuildJobPromptMissingSkill: assert "go" in result -class TestTickAdvanceBeforeRun: - """Verify that tick() calls advance_next_run before run_job for crash safety.""" - - def test_advance_called_before_run_job(self, tmp_path): - """advance_next_run must be called before run_job to prevent crash-loop re-fires.""" - call_order = [] - - def fake_advance(job_id): - call_order.append(("advance", job_id)) - return True - - def fake_run_job(job): - call_order.append(("run", job["id"])) - return True, "output", "response", None - - fake_job = { - "id": "test-advance", - "name": "test", - "prompt": "hello", - "enabled": True, - "schedule": {"kind": "cron", "expr": "15 6 * * *"}, - } - - with patch("cron.scheduler.get_due_jobs", return_value=[fake_job]), \ - patch("cron.scheduler.advance_next_run", side_effect=fake_advance) as adv_mock, \ - patch("cron.scheduler.run_job", side_effect=fake_run_job), \ - patch("cron.scheduler.save_job_output", return_value=tmp_path / "out.md"), \ - patch("cron.scheduler.mark_job_run"), \ - patch("cron.scheduler._deliver_result"): - from cron.scheduler import tick - executed = tick(verbose=False) - - assert executed == 1 - adv_mock.assert_called_once_with("test-advance") - # advance must happen before run - assert call_order == [("advance", "test-advance"), ("run", "test-advance")] - - class TestSendMediaViaAdapter: """Unit tests for _send_media_via_adapter — routes files to typed adapter methods.""" @@ -1358,12 +1205,3 @@ class TestSendMediaViaAdapter: self._run_with_loop(adapter, "123", media_files, None, {"id": "j3"}) adapter.send_voice.assert_called_once() adapter.send_image_file.assert_called_once() - - def test_single_failure_does_not_block_others(self): - adapter = MagicMock() - adapter.send_voice = AsyncMock(side_effect=RuntimeError("network error")) - adapter.send_image_file = AsyncMock() - media_files = [("/tmp/voice.ogg", False), ("/tmp/photo.png", False)] - self._run_with_loop(adapter, "123", media_files, None, {"id": "j4"}) - adapter.send_voice.assert_called_once() - adapter.send_image_file.assert_called_once() diff --git a/tests/gateway/test_bluebubbles.py b/tests/gateway/test_bluebubbles.py index a027bcd7cc..86b4ac3512 100644 --- a/tests/gateway/test_bluebubbles.py +++ b/tests/gateway/test_bluebubbles.py @@ -20,11 +20,6 @@ def _make_adapter(monkeypatch, **extra): return BlueBubblesAdapter(cfg) -class TestBlueBubblesPlatformEnum: - def test_bluebubbles_enum_exists(self): - assert Platform.BLUEBUBBLES.value == "bluebubbles" - - class TestBlueBubblesConfigLoading: def test_apply_env_overrides_bluebubbles(self, monkeypatch): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") @@ -41,15 +36,6 @@ class TestBlueBubblesConfigLoading: assert bc.extra["password"] == "secret" assert bc.extra["webhook_port"] == 9999 - def test_connected_platforms_includes_bluebubbles(self, monkeypatch): - monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") - monkeypatch.setenv("BLUEBUBBLES_PASSWORD", "secret") - from gateway.config import GatewayConfig, _apply_env_overrides - - config = GatewayConfig() - _apply_env_overrides(config) - assert Platform.BLUEBUBBLES in config.get_connected_platforms() - def test_home_channel_set_from_env(self, monkeypatch): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") monkeypatch.setenv("BLUEBUBBLES_PASSWORD", "secret") @@ -273,29 +259,6 @@ class TestBlueBubblesGuidResolution: assert result is None -class TestBlueBubblesToolsetIntegration: - def test_toolset_exists(self): - from toolsets import TOOLSETS - - assert "hermes-bluebubbles" in TOOLSETS - - def test_toolset_in_gateway_composite(self): - from toolsets import TOOLSETS - - gateway = TOOLSETS["hermes-gateway"] - assert "hermes-bluebubbles" in gateway["includes"] - - -class TestBlueBubblesPromptHint: - def test_platform_hint_exists(self): - from agent.prompt_builder import PLATFORM_HINTS - - assert "bluebubbles" in PLATFORM_HINTS - hint = PLATFORM_HINTS["bluebubbles"] - assert "iMessage" in hint - assert "plain text" in hint - - class TestBlueBubblesAttachmentDownload: """Verify _download_attachment routes to the correct cache helper.""" diff --git a/tests/gateway/test_dingtalk.py b/tests/gateway/test_dingtalk.py index 94d809850c..0894c01870 100644 --- a/tests/gateway/test_dingtalk.py +++ b/tests/gateway/test_dingtalk.py @@ -269,12 +269,6 @@ class TestConnect: # --------------------------------------------------------------------------- -class TestPlatformEnum: - - def test_dingtalk_in_platform_enum(self): - assert Platform.DINGTALK.value == "dingtalk" - - # --------------------------------------------------------------------------- # SDK compatibility regression tests (dingtalk-stream >= 0.20 / 0.24) # --------------------------------------------------------------------------- diff --git a/tests/gateway/test_email.py b/tests/gateway/test_email.py index 44e38aff43..c8eecf38ed 100644 --- a/tests/gateway/test_email.py +++ b/tests/gateway/test_email.py @@ -25,14 +25,6 @@ from unittest.mock import patch, MagicMock, AsyncMock from gateway.platforms.base import SendResult -class TestPlatformEnum(unittest.TestCase): - """Verify EMAIL is in the Platform enum.""" - - def test_email_in_platform_enum(self): - from gateway.config import Platform - self.assertEqual(Platform.EMAIL.value, "email") - - class TestConfigEnvOverrides(unittest.TestCase): """Verify email config is loaded from environment variables.""" @@ -72,20 +64,6 @@ class TestConfigEnvOverrides(unittest.TestCase): _apply_env_overrides(config) self.assertNotIn(Platform.EMAIL, config.platforms) - @patch.dict(os.environ, { - "EMAIL_ADDRESS": "hermes@test.com", - "EMAIL_PASSWORD": "secret", - "EMAIL_IMAP_HOST": "imap.test.com", - "EMAIL_SMTP_HOST": "smtp.test.com", - }, clear=False) - def test_email_in_connected_platforms(self): - from gateway.config import GatewayConfig, Platform, _apply_env_overrides - config = GatewayConfig() - _apply_env_overrides(config) - connected = config.get_connected_platforms() - self.assertIn(Platform.EMAIL, connected) - - class TestCheckRequirements(unittest.TestCase): """Verify check_email_requirements function.""" @@ -257,121 +235,6 @@ class TestExtractAttachments(unittest.TestCase): mock_cache.assert_called_once() -class TestAuthorizationMaps(unittest.TestCase): - """Verify email is in authorization maps in gateway/run.py.""" - - def test_email_in_adapter_factory(self): - """Email adapter creation branch should exist.""" - import gateway.run - import inspect - source = inspect.getsource(gateway.run.GatewayRunner._create_adapter) - self.assertIn("Platform.EMAIL", source) - - def test_email_in_allowed_users_map(self): - """EMAIL_ALLOWED_USERS should be in platform_env_map.""" - import gateway.run - import inspect - source = inspect.getsource(gateway.run.GatewayRunner._is_user_authorized) - self.assertIn("EMAIL_ALLOWED_USERS", source) - - def test_email_in_allow_all_map(self): - """EMAIL_ALLOW_ALL_USERS should be in platform_allow_all_map.""" - import gateway.run - import inspect - source = inspect.getsource(gateway.run.GatewayRunner._is_user_authorized) - self.assertIn("EMAIL_ALLOW_ALL_USERS", source) - - -class TestSendMessageToolRouting(unittest.TestCase): - """Verify email routing in send_message_tool.""" - - def test_email_in_platform_map(self): - import tools.send_message_tool as smt - import inspect - source = inspect.getsource(smt._handle_send) - self.assertIn('"email"', source) - - def test_send_to_platform_has_email_branch(self): - import tools.send_message_tool as smt - import inspect - source = inspect.getsource(smt._send_to_platform) - self.assertIn("Platform.EMAIL", source) - - -class TestCronDelivery(unittest.TestCase): - """Verify email in cron scheduler platform_map.""" - - def test_email_in_cron_platform_map(self): - import cron.scheduler - import inspect - source = inspect.getsource(cron.scheduler) - self.assertIn('"email"', source) - - -class TestToolset(unittest.TestCase): - """Verify email toolset is registered.""" - - def test_email_toolset_exists(self): - from toolsets import TOOLSETS - self.assertIn("hermes-email", TOOLSETS) - - def test_email_in_gateway_toolset(self): - from toolsets import TOOLSETS - includes = TOOLSETS["hermes-gateway"]["includes"] - self.assertIn("hermes-email", includes) - - -class TestPlatformHints(unittest.TestCase): - """Verify email platform hint is registered.""" - - def test_email_in_platform_hints(self): - from agent.prompt_builder import PLATFORM_HINTS - self.assertIn("email", PLATFORM_HINTS) - self.assertIn("email", PLATFORM_HINTS["email"].lower()) - - -class TestChannelDirectory(unittest.TestCase): - """Verify email in channel directory session-based discovery.""" - - def test_email_in_session_discovery(self): - from gateway.config import Platform - # Verify email is a Platform enum member — the dynamic loop in - # build_channel_directory iterates all Platform members, so email - # is included automatically as long as it's in the enum. - email_values = [p.value for p in Platform] - self.assertIn("email", email_values) - - -class TestGatewaySetup(unittest.TestCase): - """Verify email in gateway setup wizard.""" - - def test_email_in_platforms_list(self): - from hermes_cli.gateway import _PLATFORMS - keys = [p["key"] for p in _PLATFORMS] - self.assertIn("email", keys) - - def test_email_has_setup_vars(self): - from hermes_cli.gateway import _PLATFORMS - email_platform = next(p for p in _PLATFORMS if p["key"] == "email") - var_names = [v["name"] for v in email_platform["vars"]] - self.assertIn("EMAIL_ADDRESS", var_names) - self.assertIn("EMAIL_PASSWORD", var_names) - self.assertIn("EMAIL_IMAP_HOST", var_names) - self.assertIn("EMAIL_SMTP_HOST", var_names) - - -class TestEnvExample(unittest.TestCase): - """Verify .env.example has email config.""" - - def test_env_example_has_email_vars(self): - env_path = Path(__file__).resolve().parents[2] / ".env.example" - content = env_path.read_text() - self.assertIn("EMAIL_ADDRESS", content) - self.assertIn("EMAIL_PASSWORD", content) - self.assertIn("EMAIL_IMAP_HOST", content) - self.assertIn("EMAIL_SMTP_HOST", content) - - class TestDispatchMessage(unittest.TestCase): """Test email message dispatch logic.""" diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 06665610b2..c5a6d8a550 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -29,13 +29,6 @@ def _mock_event_dispatcher_builder(mock_handler_class): return mock_builder -class TestPlatformEnum(unittest.TestCase): - def test_feishu_in_platform_enum(self): - from gateway.config import Platform - - self.assertEqual(Platform.FEISHU.value, "feishu") - - class TestConfigEnvOverrides(unittest.TestCase): @patch.dict(os.environ, { "FEISHU_APP_ID": "cli_xxx", @@ -82,24 +75,6 @@ class TestConfigEnvOverrides(unittest.TestCase): self.assertIn(Platform.FEISHU, config.get_connected_platforms()) -class TestGatewayIntegration(unittest.TestCase): - def test_feishu_in_adapter_factory(self): - source = Path("gateway/run.py").read_text(encoding="utf-8") - self.assertIn("Platform.FEISHU", source) - self.assertIn("FeishuAdapter", source) - - def test_feishu_in_authorization_maps(self): - source = Path("gateway/run.py").read_text(encoding="utf-8") - self.assertIn("FEISHU_ALLOWED_USERS", source) - self.assertIn("FEISHU_ALLOW_ALL_USERS", source) - - def test_feishu_toolset_exists(self): - from toolsets import TOOLSETS - - self.assertIn("hermes-feishu", TOOLSETS) - self.assertIn("hermes-feishu", TOOLSETS["hermes-gateway"]["includes"]) - - class TestFeishuMessageNormalization(unittest.TestCase): def test_normalize_merge_forward_preserves_summary_lines(self): from gateway.platforms.feishu import normalize_feishu_message @@ -472,27 +447,6 @@ class TestFeishuAdapterMessaging(unittest.TestCase): self.assertEqual(info["type"], "group") class TestAdapterModule(unittest.TestCase): - def test_adapter_requirement_helper_exists(self): - source = Path("gateway/platforms/feishu.py").read_text(encoding="utf-8") - self.assertIn("def check_feishu_requirements()", source) - self.assertIn("FEISHU_AVAILABLE", source) - - def test_adapter_declares_websocket_scope(self): - source = Path("gateway/platforms/feishu.py").read_text(encoding="utf-8") - self.assertIn("Supported modes: websocket, webhook", source) - self.assertIn("FEISHU_CONNECTION_MODE", source) - - def test_adapter_registers_message_read_noop_handler(self): - source = Path("gateway/platforms/feishu.py").read_text(encoding="utf-8") - self.assertIn("register_p2_im_message_message_read_v1", source) - self.assertIn("def _on_message_read_event", source) - - def test_adapter_registers_reaction_and_card_handlers_for_websocket(self): - source = Path("gateway/platforms/feishu.py").read_text(encoding="utf-8") - self.assertIn("register_p2_im_message_reaction_created_v1", source) - self.assertIn("register_p2_im_message_reaction_deleted_v1", source) - self.assertIn("register_p2_card_action_trigger", source) - def test_load_settings_uses_sdk_defaults_for_invalid_ws_reconnect_values(self): from gateway.platforms.feishu import FeishuAdapter diff --git a/tests/gateway/test_homeassistant.py b/tests/gateway/test_homeassistant.py index f92da00392..b4ff5d8a35 100644 --- a/tests/gateway/test_homeassistant.py +++ b/tests/gateway/test_homeassistant.py @@ -469,18 +469,6 @@ class TestConfigIntegration: assert ha.extra["watch_domains"] == ["climate"] assert ha.extra["cooldown_seconds"] == 45 - def test_connected_platforms_includes_ha(self): - config = GatewayConfig( - platforms={ - Platform.HOMEASSISTANT: PlatformConfig(enabled=True, token="tok"), - Platform.TELEGRAM: PlatformConfig(enabled=False, token="t"), - }, - ) - connected = config.get_connected_platforms() - assert Platform.HOMEASSISTANT in connected - assert Platform.TELEGRAM not in connected - - # --------------------------------------------------------------------------- # send() via REST API # --------------------------------------------------------------------------- @@ -582,27 +570,6 @@ class TestSendViaRestApi: # --------------------------------------------------------------------------- -class TestToolsetIntegration: - def test_homeassistant_toolset_resolves(self): - from toolsets import resolve_toolset - - tools = resolve_toolset("homeassistant") - assert set(tools) == {"ha_list_entities", "ha_get_state", "ha_call_service", "ha_list_services"} - - def test_gateway_toolset_includes_ha_tools(self): - from toolsets import resolve_toolset - - gateway_tools = resolve_toolset("hermes-gateway") - for tool in ("ha_list_entities", "ha_get_state", "ha_call_service", "ha_list_services"): - assert tool in gateway_tools - - def test_hermes_core_tools_includes_ha(self): - from toolsets import _HERMES_CORE_TOOLS - - for tool in ("ha_list_entities", "ha_get_state", "ha_call_service", "ha_list_services"): - assert tool in _HERMES_CORE_TOOLS - - # --------------------------------------------------------------------------- # WebSocket URL construction # --------------------------------------------------------------------------- diff --git a/tests/gateway/test_matrix.py b/tests/gateway/test_matrix.py index 845c0fff1f..a088ad9ba8 100644 --- a/tests/gateway/test_matrix.py +++ b/tests/gateway/test_matrix.py @@ -239,15 +239,6 @@ def _make_fake_mautrix(): # Platform & Config # --------------------------------------------------------------------------- -class TestMatrixPlatformEnum: - def test_matrix_enum_exists(self): - assert Platform.MATRIX.value == "matrix" - - def test_matrix_in_platform_list(self): - platforms = [p.value for p in Platform] - assert "matrix" in platforms - - class TestMatrixConfigLoading: def test_apply_env_overrides_with_access_token(self, monkeypatch): monkeypatch.setenv("MATRIX_ACCESS_TOKEN", "syt_abc123") diff --git a/tests/gateway/test_mattermost.py b/tests/gateway/test_mattermost.py index 56e46f6364..1ed79a5b2e 100644 --- a/tests/gateway/test_mattermost.py +++ b/tests/gateway/test_mattermost.py @@ -12,15 +12,6 @@ from gateway.config import Platform, PlatformConfig # Platform & Config # --------------------------------------------------------------------------- -class TestMattermostPlatformEnum: - def test_mattermost_enum_exists(self): - assert Platform.MATTERMOST.value == "mattermost" - - def test_mattermost_in_platform_list(self): - platforms = [p.value for p in Platform] - assert "mattermost" in platforms - - class TestMattermostConfigLoading: def test_apply_env_overrides_mattermost(self, monkeypatch): monkeypatch.setenv("MATTERMOST_TOKEN", "mm-tok-abc123") @@ -46,17 +37,6 @@ class TestMattermostConfigLoading: assert Platform.MATTERMOST not in config.platforms - def test_connected_platforms_includes_mattermost(self, monkeypatch): - monkeypatch.setenv("MATTERMOST_TOKEN", "mm-tok-abc123") - monkeypatch.setenv("MATTERMOST_URL", "https://mm.example.com") - - from gateway.config import GatewayConfig, _apply_env_overrides - config = GatewayConfig() - _apply_env_overrides(config) - - connected = config.get_connected_platforms() - assert Platform.MATTERMOST in connected - def test_mattermost_home_channel(self, monkeypatch): monkeypatch.setenv("MATTERMOST_TOKEN", "mm-tok-abc123") monkeypatch.setenv("MATTERMOST_URL", "https://mm.example.com") diff --git a/tests/gateway/test_signal.py b/tests/gateway/test_signal.py index 265f9be783..26f1e4f3bb 100644 --- a/tests/gateway/test_signal.py +++ b/tests/gateway/test_signal.py @@ -42,15 +42,6 @@ def _stub_rpc(return_value): # Platform & Config # --------------------------------------------------------------------------- -class TestSignalPlatformEnum: - def test_signal_enum_exists(self): - assert Platform.SIGNAL.value == "signal" - - def test_signal_in_platform_list(self): - platforms = [p.value for p in Platform] - assert "signal" in platforms - - class TestSignalConfigLoading: def test_apply_env_overrides_signal(self, monkeypatch): monkeypatch.setenv("SIGNAL_HTTP_URL", "http://localhost:9090") @@ -76,18 +67,6 @@ class TestSignalConfigLoading: assert Platform.SIGNAL not in config.platforms - def test_connected_platforms_includes_signal(self, monkeypatch): - monkeypatch.setenv("SIGNAL_HTTP_URL", "http://localhost:8080") - monkeypatch.setenv("SIGNAL_ACCOUNT", "+15551234567") - - from gateway.config import GatewayConfig, _apply_env_overrides - config = GatewayConfig() - _apply_env_overrides(config) - - connected = config.get_connected_platforms() - assert Platform.SIGNAL in connected - - # --------------------------------------------------------------------------- # Adapter Init & Helpers # --------------------------------------------------------------------------- @@ -362,15 +341,6 @@ class TestSignalAuthorization: # Send Message Tool # --------------------------------------------------------------------------- -class TestSignalSendMessage: - def test_signal_in_platform_map(self): - """Signal should be in the send_message tool's platform map.""" - from tools.send_message_tool import send_message_tool - # Just verify the import works and Signal is a valid platform - from gateway.config import Platform - assert Platform.SIGNAL.value == "signal" - - # --------------------------------------------------------------------------- # send_image_file method (#5105) # --------------------------------------------------------------------------- diff --git a/tests/gateway/test_sms.py b/tests/gateway/test_sms.py index d8a1589bdf..524d540f81 100644 --- a/tests/gateway/test_sms.py +++ b/tests/gateway/test_sms.py @@ -20,9 +20,6 @@ from gateway.config import Platform, PlatformConfig, HomeChannel class TestSmsConfigLoading: """Verify _apply_env_overrides wires SMS correctly.""" - def test_sms_platform_enum_exists(self): - assert Platform.SMS.value == "sms" - def test_env_overrides_create_sms_config(self): from gateway.config import load_gateway_config @@ -56,19 +53,6 @@ class TestSmsConfigLoading: assert hc.name == "My Phone" assert hc.platform == Platform.SMS - def test_sms_in_connected_platforms(self): - from gateway.config import load_gateway_config - - env = { - "TWILIO_ACCOUNT_SID": "ACtest123", - "TWILIO_AUTH_TOKEN": "token_abc", - } - with patch.dict(os.environ, env, clear=False): - config = load_gateway_config() - connected = config.get_connected_platforms() - assert Platform.SMS in connected - - # ── Format / truncate ─────────────────────────────────────────────── class TestSmsFormatAndTruncate: @@ -180,44 +164,6 @@ class TestSmsRequirements: # ── Toolset verification ─────────────────────────────────────────── -class TestSmsToolset: - def test_hermes_sms_toolset_exists(self): - from toolsets import get_toolset - - ts = get_toolset("hermes-sms") - assert ts is not None - assert "tools" in ts - - def test_hermes_sms_in_gateway_includes(self): - from toolsets import get_toolset - - gw = get_toolset("hermes-gateway") - assert gw is not None - assert "hermes-sms" in gw["includes"] - - def test_sms_platform_hint_exists(self): - from agent.prompt_builder import PLATFORM_HINTS - - assert "sms" in PLATFORM_HINTS - assert "concise" in PLATFORM_HINTS["sms"].lower() - - def test_sms_in_scheduler_platform_map(self): - """Verify cron scheduler recognizes 'sms' as a valid platform.""" - # Just check the Platform enum has SMS — the scheduler imports it dynamically - assert Platform.SMS.value == "sms" - - def test_sms_in_send_message_platform_map(self): - """Verify send_message_tool recognizes 'sms'.""" - # The platform_map is built inside _handle_send; verify SMS enum exists - assert hasattr(Platform, "SMS") - - def test_sms_in_cronjob_deliver_description(self): - """Verify cronjob_tools mentions sms in deliver description.""" - from tools.cronjob_tools import CRONJOB_SCHEMA - deliver_desc = CRONJOB_SCHEMA["parameters"]["properties"]["deliver"]["description"] - assert "sms" in deliver_desc.lower() - - # ── Webhook host configuration ───────────────────────────────────── class TestWebhookHostConfig: diff --git a/tests/gateway/test_wecom.py b/tests/gateway/test_wecom.py index 0540146d7c..cc4aaddc7a 100644 --- a/tests/gateway/test_wecom.py +++ b/tests/gateway/test_wecom.py @@ -593,7 +593,3 @@ class TestInboundMessages: await adapter._on_message(payload) adapter.handle_message.assert_not_awaited() - -class TestPlatformEnum: - def test_wecom_in_platform_enum(self): - assert Platform.WECOM.value == "wecom" diff --git a/tests/hermes_cli/test_argparse_flag_propagation.py b/tests/hermes_cli/test_argparse_flag_propagation.py index 388f3aef50..7787fdd6ff 100644 --- a/tests/hermes_cli/test_argparse_flag_propagation.py +++ b/tests/hermes_cli/test_argparse_flag_propagation.py @@ -57,85 +57,6 @@ def _build_parser(): return parser -class TestFlagBeforeSubcommand: - """Flags placed before 'chat' must propagate through.""" - - def test_yolo_before_chat(self): - parser = _build_parser() - args = parser.parse_args(["--yolo", "chat"]) - assert getattr(args, "yolo", False) is True - - def test_worktree_before_chat(self): - parser = _build_parser() - args = parser.parse_args(["-w", "chat"]) - assert getattr(args, "worktree", False) is True - - def test_skills_before_chat(self): - parser = _build_parser() - args = parser.parse_args(["-s", "myskill", "chat"]) - assert getattr(args, "skills", None) == ["myskill"] - - def test_pass_session_id_before_chat(self): - parser = _build_parser() - args = parser.parse_args(["--pass-session-id", "chat"]) - assert getattr(args, "pass_session_id", False) is True - - def test_resume_before_chat(self): - parser = _build_parser() - args = parser.parse_args(["-r", "abc123", "chat"]) - assert getattr(args, "resume", None) == "abc123" - - -class TestFlagAfterSubcommand: - """Flags placed after 'chat' must still work.""" - - def test_yolo_after_chat(self): - parser = _build_parser() - args = parser.parse_args(["chat", "--yolo"]) - assert getattr(args, "yolo", False) is True - - def test_worktree_after_chat(self): - parser = _build_parser() - args = parser.parse_args(["chat", "-w"]) - assert getattr(args, "worktree", False) is True - - def test_skills_after_chat(self): - parser = _build_parser() - args = parser.parse_args(["chat", "-s", "myskill"]) - assert getattr(args, "skills", None) == ["myskill"] - - def test_resume_after_chat(self): - parser = _build_parser() - args = parser.parse_args(["chat", "-r", "abc123"]) - assert getattr(args, "resume", None) == "abc123" - - -class TestNoSubcommandDefaults: - """When no subcommand is given, flags must work and defaults must hold.""" - - def test_yolo_no_subcommand(self): - parser = _build_parser() - args = parser.parse_args(["--yolo"]) - assert args.yolo is True - assert args.command is None - - def test_defaults_no_flags(self): - parser = _build_parser() - args = parser.parse_args([]) - assert getattr(args, "yolo", False) is False - assert getattr(args, "worktree", False) is False - assert getattr(args, "skills", None) is None - assert getattr(args, "resume", None) is None - - def test_defaults_chat_no_flags(self): - parser = _build_parser() - args = parser.parse_args(["chat"]) - # With SUPPRESS, these fall through to parent defaults - assert getattr(args, "yolo", False) is False - assert getattr(args, "worktree", False) is False - assert getattr(args, "skills", None) is None - - class TestYoloEnvVar: """Verify --yolo sets HERMES_YOLO_MODE regardless of flag position. diff --git a/tests/hermes_cli/test_debug.py b/tests/hermes_cli/test_debug.py index 864a64160f..e01b7a41aa 100644 --- a/tests/hermes_cli/test_debug.py +++ b/tests/hermes_cli/test_debug.py @@ -449,20 +449,6 @@ class TestRunDebug: # Argparse integration # --------------------------------------------------------------------------- -class TestArgparseIntegration: - def test_module_imports_clean(self): - from hermes_cli.debug import run_debug, run_debug_share - assert callable(run_debug) - assert callable(run_debug_share) - - def test_cmd_debug_dispatches(self): - from hermes_cli.main import cmd_debug - - args = MagicMock() - args.debug_command = None - cmd_debug(args) - - # --------------------------------------------------------------------------- # Delete / auto-delete # --------------------------------------------------------------------------- diff --git a/tests/hermes_cli/test_plugin_cli_registration.py b/tests/hermes_cli/test_plugin_cli_registration.py index 4b0aea5f9f..af923b96a0 100644 --- a/tests/hermes_cli/test_plugin_cli_registration.py +++ b/tests/hermes_cli/test_plugin_cli_registration.py @@ -173,60 +173,6 @@ class TestMemoryPluginCliDiscovery: # ── Honcho register_cli ────────────────────────────────────────────────── -class TestHonchoRegisterCli: - def test_builds_subcommand_tree(self): - """register_cli creates the expected subparser tree.""" - from plugins.memory.honcho.cli import register_cli - - parser = argparse.ArgumentParser() - register_cli(parser) - - # Verify key subcommands exist by parsing them - args = parser.parse_args(["status"]) - assert args.honcho_command == "status" - - args = parser.parse_args(["peer", "--user", "alice"]) - assert args.honcho_command == "peer" - assert args.user == "alice" - - args = parser.parse_args(["mode", "tools"]) - assert args.honcho_command == "mode" - assert args.mode == "tools" - - args = parser.parse_args(["tokens", "--context", "500"]) - assert args.honcho_command == "tokens" - assert args.context == 500 - - args = parser.parse_args(["--target-profile", "coder", "status"]) - assert args.target_profile == "coder" - assert args.honcho_command == "status" - - def test_setup_redirects_to_memory_setup(self): - """hermes honcho setup redirects to memory setup.""" - from plugins.memory.honcho.cli import register_cli - - parser = argparse.ArgumentParser() - register_cli(parser) - args = parser.parse_args(["setup"]) - assert args.honcho_command == "setup" - - def test_mode_choices_are_recall_modes(self): - """Mode subcommand uses recall mode choices (hybrid/context/tools).""" - from plugins.memory.honcho.cli import register_cli - - parser = argparse.ArgumentParser() - register_cli(parser) - - # Valid recall modes should parse - for mode in ("hybrid", "context", "tools"): - args = parser.parse_args(["mode", mode]) - assert args.mode == mode - - # Old memoryMode values should fail - with pytest.raises(SystemExit): - parser.parse_args(["mode", "honcho"]) - - # ── ProviderCollector no-op ────────────────────────────────────────────── diff --git a/tests/hermes_cli/test_plugins_cmd.py b/tests/hermes_cli/test_plugins_cmd.py index 1ccf786e3a..72b9bdde2c 100644 --- a/tests/hermes_cli/test_plugins_cmd.py +++ b/tests/hermes_cli/test_plugins_cmd.py @@ -126,59 +126,6 @@ class TestRepoNameFromUrl: # ── plugins_command dispatch ────────────────────────────────────────────── -class TestPluginsCommandDispatch: - """Verify alias routing in plugins_command().""" - - def _make_args(self, action, **extras): - args = MagicMock() - args.plugins_action = action - for k, v in extras.items(): - setattr(args, k, v) - return args - - @patch("hermes_cli.plugins_cmd.cmd_remove") - def test_rm_alias(self, mock_remove): - args = self._make_args("rm", name="some-plugin") - plugins_command(args) - mock_remove.assert_called_once_with("some-plugin") - - @patch("hermes_cli.plugins_cmd.cmd_remove") - def test_uninstall_alias(self, mock_remove): - args = self._make_args("uninstall", name="some-plugin") - plugins_command(args) - mock_remove.assert_called_once_with("some-plugin") - - @patch("hermes_cli.plugins_cmd.cmd_list") - def test_ls_alias(self, mock_list): - args = self._make_args("ls") - plugins_command(args) - mock_list.assert_called_once() - - @patch("hermes_cli.plugins_cmd.cmd_toggle") - def test_none_falls_through_to_toggle(self, mock_toggle): - args = self._make_args(None) - plugins_command(args) - mock_toggle.assert_called_once() - - @patch("hermes_cli.plugins_cmd.cmd_install") - def test_install_dispatches(self, mock_install): - args = self._make_args("install", identifier="owner/repo", force=False) - plugins_command(args) - mock_install.assert_called_once_with("owner/repo", force=False) - - @patch("hermes_cli.plugins_cmd.cmd_update") - def test_update_dispatches(self, mock_update): - args = self._make_args("update", name="foo") - plugins_command(args) - mock_update.assert_called_once_with("foo") - - @patch("hermes_cli.plugins_cmd.cmd_remove") - def test_remove_dispatches(self, mock_remove): - args = self._make_args("remove", name="bar") - plugins_command(args) - mock_remove.assert_called_once_with("bar") - - # ── _read_manifest ──────────────────────────────────────────────────────── diff --git a/tests/hermes_cli/test_subparser_routing_fallback.py b/tests/hermes_cli/test_subparser_routing_fallback.py index ba907ca123..37b3509f13 100644 --- a/tests/hermes_cli/test_subparser_routing_fallback.py +++ b/tests/hermes_cli/test_subparser_routing_fallback.py @@ -64,85 +64,3 @@ def _safe_parse(parser, subparsers, argv): subparsers.required = False return parser.parse_args(argv) - -class TestSubparserRoutingFallback: - """Verify the bpo-9338 defensive routing works for all key cases.""" - - def test_direct_subcommand(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["model"]) - assert args.command == "model" - - def test_subcommand_with_flags(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["--yolo", "model"]) - assert args.command == "model" - assert args.yolo is True - - def test_bare_hermes_defaults_to_none(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, []) - assert args.command is None - - def test_flags_only_defaults_to_none(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["--yolo"]) - assert args.command is None - assert args.yolo is True - - def test_continue_flag_alone(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-c"]) - assert args.command is None - assert args.continue_last is True - - def test_continue_with_session_name(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-c", "myproject"]) - assert args.command is None - assert args.continue_last == "myproject" - - def test_continue_with_subcommand_name_as_session(self): - """Edge case: session named 'model' — should be treated as session name, not subcommand.""" - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-c", "model"]) - assert args.command is None - assert args.continue_last == "model" - - def test_continue_with_session_then_subcommand(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-c", "myproject", "model"]) - assert args.command == "model" - assert args.continue_last == "myproject" - - def test_chat_with_query(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["chat", "-q", "hello"]) - assert args.command == "chat" - assert args.query == "hello" - - def test_resume_flag(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-r", "abc123"]) - assert args.command is None - assert args.resume == "abc123" - - def test_resume_with_subcommand(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-r", "abc123", "chat"]) - assert args.command == "chat" - assert args.resume == "abc123" - - def test_skills_flag_with_subcommand(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["-s", "myskill", "chat"]) - assert args.command == "chat" - assert args.skills == ["myskill"] - - def test_all_flags_with_subcommand(self): - parser, sub = _build_parser() - args = _safe_parse(parser, sub, ["--yolo", "-w", "-s", "myskill", "model"]) - assert args.command == "model" - assert args.yolo is True - assert args.worktree is True - assert args.skills == ["myskill"] diff --git a/tests/plugins/test_retaindb_plugin.py b/tests/plugins/test_retaindb_plugin.py index 7e334709f6..9ad801769b 100644 --- a/tests/plugins/test_retaindb_plugin.py +++ b/tests/plugins/test_retaindb_plugin.py @@ -83,34 +83,6 @@ class TestClient: assert h["Authorization"] == "Bearer rdb-test-key" assert h["X-API-Key"] == "rdb-test-key" - def test_query_context_builds_correct_payload(self): - c = self._make_client() - with patch.object(c, "request") as mock_req: - mock_req.return_value = {"results": []} - c.query_context("user1", "sess1", "test query", max_tokens=500) - mock_req.assert_called_once_with("POST", "/v1/context/query", json_body={ - "project": "test", - "query": "test query", - "user_id": "user1", - "session_id": "sess1", - "include_memories": True, - "max_tokens": 500, - }) - - def test_search_builds_correct_payload(self): - c = self._make_client() - with patch.object(c, "request") as mock_req: - mock_req.return_value = {"results": []} - c.search("user1", "sess1", "find this", top_k=5) - mock_req.assert_called_once_with("POST", "/v1/memory/search", json_body={ - "project": "test", - "query": "find this", - "user_id": "user1", - "session_id": "sess1", - "top_k": 5, - "include_pending": True, - }) - def test_add_memory_tries_fallback(self): c = self._make_client() call_count = 0 @@ -141,40 +113,6 @@ class TestClient: assert result == {"deleted": True} assert call_count == 2 - def test_ingest_session_payload(self): - c = self._make_client() - with patch.object(c, "request") as mock_req: - mock_req.return_value = {"status": "ok"} - msgs = [{"role": "user", "content": "hi"}] - c.ingest_session("u1", "s1", msgs, timeout=10.0) - mock_req.assert_called_once_with("POST", "/v1/memory/ingest/session", json_body={ - "project": "test", - "session_id": "s1", - "user_id": "u1", - "messages": msgs, - "write_mode": "sync", - }, timeout=10.0) - - def test_ask_user_payload(self): - c = self._make_client() - with patch.object(c, "request") as mock_req: - mock_req.return_value = {"answer": "test answer"} - c.ask_user("u1", "who am i?", reasoning_level="medium") - mock_req.assert_called_once() - call_kwargs = mock_req.call_args - assert call_kwargs[1]["json_body"]["reasoning_level"] == "medium" - - def test_get_agent_model_path(self): - c = self._make_client() - with patch.object(c, "request") as mock_req: - mock_req.return_value = {"memory_count": 3} - c.get_agent_model("hermes") - mock_req.assert_called_once_with( - "GET", "/v1/memory/agent/hermes/model", - params={"project": "test"}, timeout=4.0 - ) - - # =========================================================================== # _WriteQueue tests # =========================================================================== @@ -413,22 +351,6 @@ class TestRetainDBMemoryProvider: assert "Active" in block p.shutdown() - def test_tool_schemas_count(self, tmp_path, monkeypatch): - p = self._make_provider(tmp_path, monkeypatch) - schemas = p.get_tool_schemas() - assert len(schemas) == 10 # 5 memory + 5 file tools - names = [s["name"] for s in schemas] - assert "retaindb_profile" in names - assert "retaindb_search" in names - assert "retaindb_context" in names - assert "retaindb_remember" in names - assert "retaindb_forget" in names - assert "retaindb_upload_file" in names - assert "retaindb_list_files" in names - assert "retaindb_read_file" in names - assert "retaindb_ingest_file" in names - assert "retaindb_delete_file" in names - def test_handle_tool_call_not_initialized(self): p = RetainDBMemoryProvider() result = json.loads(p.handle_tool_call("retaindb_profile", {})) diff --git a/tests/tools/test_web_tools_config.py b/tests/tools/test_web_tools_config.py index ff9e0d5491..7fcf700d55 100644 --- a/tests/tools/test_web_tools_config.py +++ b/tests/tools/test_web_tools_config.py @@ -63,38 +63,6 @@ class TestFirecrawlClientConfig: # ── Configuration matrix ───────────────────────────────────────── - def test_cloud_mode_key_only(self): - """API key without URL → cloud Firecrawl.""" - with patch.dict(os.environ, {"FIRECRAWL_API_KEY": "fc-test"}): - with patch("tools.web_tools.Firecrawl") as mock_fc: - from tools.web_tools import _get_firecrawl_client - result = _get_firecrawl_client() - mock_fc.assert_called_once_with(api_key="fc-test") - assert result is mock_fc.return_value - - def test_self_hosted_with_key(self): - """Both key + URL → self-hosted with auth.""" - with patch.dict(os.environ, { - "FIRECRAWL_API_KEY": "fc-test", - "FIRECRAWL_API_URL": "http://localhost:3002", - }): - with patch("tools.web_tools.Firecrawl") as mock_fc: - from tools.web_tools import _get_firecrawl_client - result = _get_firecrawl_client() - mock_fc.assert_called_once_with( - api_key="fc-test", api_url="http://localhost:3002" - ) - assert result is mock_fc.return_value - - def test_self_hosted_no_key(self): - """URL only, no key → self-hosted without auth.""" - with patch.dict(os.environ, {"FIRECRAWL_API_URL": "http://localhost:3002"}): - with patch("tools.web_tools.Firecrawl") as mock_fc: - from tools.web_tools import _get_firecrawl_client - result = _get_firecrawl_client() - mock_fc.assert_called_once_with(api_url="http://localhost:3002") - assert result is mock_fc.return_value - def test_no_config_raises_with_helpful_message(self): """Neither key nor URL → ValueError with guidance.""" with patch("tools.web_tools.Firecrawl"): @@ -169,18 +137,6 @@ class TestFirecrawlClientConfig: api_url="https://firecrawl-gateway.nousresearch.com", ) - def test_direct_mode_is_preferred_over_tool_gateway(self): - """Explicit Firecrawl config should win over the gateway fallback.""" - with patch.dict(os.environ, { - "FIRECRAWL_API_KEY": "fc-test", - "TOOL_GATEWAY_DOMAIN": "nousresearch.com", - }): - with patch("tools.web_tools._read_nous_access_token", return_value="nous-token"): - with patch("tools.web_tools.Firecrawl") as mock_fc: - from tools.web_tools import _get_firecrawl_client - _get_firecrawl_client() - mock_fc.assert_called_once_with(api_key="fc-test") - def test_nous_auth_token_respects_hermes_home_override(self, tmp_path): """Auth lookup should read from HERMES_HOME/auth.json, not ~/.hermes/auth.json.""" real_home = tmp_path / "real-home" @@ -275,18 +231,6 @@ class TestFirecrawlClientConfig: # ── Edge cases ─────────────────────────────────────────────────── - def test_empty_string_key_treated_as_absent(self): - """FIRECRAWL_API_KEY='' should not be passed as api_key.""" - with patch.dict(os.environ, { - "FIRECRAWL_API_KEY": "", - "FIRECRAWL_API_URL": "http://localhost:3002", - }): - with patch("tools.web_tools.Firecrawl") as mock_fc: - from tools.web_tools import _get_firecrawl_client - _get_firecrawl_client() - # Empty string is falsy, so only api_url should be passed - mock_fc.assert_called_once_with(api_url="http://localhost:3002") - def test_empty_string_key_no_url_raises(self): """FIRECRAWL_API_KEY='' with no URL → should raise.""" with patch.dict(os.environ, {"FIRECRAWL_API_KEY": ""}):