From ba030bc0dbcbfddeccf4774af7b76360bae3947a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:33:28 -0700 Subject: [PATCH] =?UTF-8?q?fix(test-seams):=20monkeypatch.setattr=20facade?= =?UTF-8?q?=20aliases=20=E2=80=94=20also=20patch=20the=20defining=20module?= =?UTF-8?q?=20(57=20files)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests did monkeypatch.setattr(, name) where name is now defined in a sibling module and the production path reads the sibling's binding. Where production reads through BOTH bindings the setattr is duplicated onto the defining module (import added next to the existing alias import); where only the sibling reads it the target is repointed. Seams whose production readers go through the facade are left alone. --- .../agent/test_auxiliary_client_ssl_verify.py | 6 +++ ...t_credential_pool_nous_refresh_stampede.py | 4 ++ ...test_credential_pool_oauth_writethrough.py | 2 + tests/cli/test_cli_provider_resolution.py | 2 + tests/cron/test_restart_safe_worker.py | 6 +++ .../test_api_server_active_work_drain.py | 4 ++ tests/gateway/test_cron_active_work_drain.py | 2 + tests/gateway/test_gateway_shutdown.py | 2 + tests/hermes_cli/test_approval_transport.py | 14 +++++++ tests/hermes_cli/test_approvals_test.py | 2 + .../hermes_cli/test_auth_codex_quota_probe.py | 5 +++ tests/hermes_cli/test_auth_codex_self_heal.py | 4 ++ .../hermes_cli/test_auth_loopback_ssh_hint.py | 3 ++ tests/hermes_cli/test_auth_nous_provider.py | 20 +++++++++ .../test_auth_store_windows_encoding.py | 2 + tests/hermes_cli/test_aux_config.py | 2 + tests/hermes_cli/test_billing_scope_stepup.py | 4 ++ .../test_checkout_mutation_guards.py | 2 + tests/hermes_cli/test_computer_use_cli.py | 29 +++++++++++++ .../test_imagegen_managed_gateway.py | 2 + .../test_lazy_refresh_venv_repair.py | 10 +++++ .../hermes_cli/test_list_picker_providers.py | 4 ++ .../test_nous_inference_url_validation.py | 20 +++++++++ tests/hermes_cli/test_nous_policy_surfaces.py | 13 ++++++ .../test_nous_portal_staging_allowlist.py | 2 + .../hermes_cli/test_nous_session_validity.py | 6 +++ .../test_relay_shared_metrics_runtime.py | 5 +++ .../test_serve_mcp_discovery_after_bind.py | 2 + .../test_serve_runtime_inventory.py | 10 +++++ .../test_shim_fail_closed_windows_live.py | 12 ++++++ .../test_sibling_config_migration.py | 4 ++ tests/hermes_cli/test_spotify_auth.py | 11 +++++ tests/hermes_cli/test_subscription_cli.py | 2 + tests/hermes_cli/test_update_autostash.py | 20 +++++++++ ...test_update_cold_start_gateway_liveness.py | 2 + .../test_update_concurrent_quarantine.py | 41 +++++++++++++++++++ .../test_update_desktop_stale_warning.py | 7 ++++ .../test_update_fleet_restart_pending.py | 26 ++++++++++++ .../hermes_cli/test_update_head_moved_gate.py | 7 ++++ tests/hermes_cli/test_update_import_guard.py | 13 ++++++ .../test_update_interrupted_recovery.py | 8 ++++ .../test_update_parked_branch_guard.py | 4 ++ .../test_update_sqlite_remediation.py | 24 +++++++++++ .../test_update_venv_ownership_preflight.py | 13 ++++++ .../test_update_zip_fallback_guards.py | 4 ++ ...est_web_routers_tools_install_on_enable.py | 12 ++++-- ...ws_gateway_cold_start_desktop_lifecycle.py | 8 ++++ ...test_windows_gateway_job_teardown_48820.py | 2 + ...t_windows_update_restart_reconciliation.py | 4 ++ tests/test_resource_limits.py | 4 ++ tests/tools/test_approval_deny_rules.py | 3 ++ tests/tools/test_delegate_kanban_isolation.py | 2 + tests/tools/test_denial_circuit_breaker.py | 12 ++++++ .../test_execute_code_approval_cluster.py | 14 +++++++ tests/tools/test_request_tool_approval.py | 30 ++++++++++++++ tests/tools/test_skills_sync_client.py | 23 +++++++++++ tests/tools/test_xai_http_credentials.py | 2 + 57 files changed, 499 insertions(+), 4 deletions(-) diff --git a/tests/agent/test_auxiliary_client_ssl_verify.py b/tests/agent/test_auxiliary_client_ssl_verify.py index 1439f33c8a..a1f58c21aa 100644 --- a/tests/agent/test_auxiliary_client_ssl_verify.py +++ b/tests/agent/test_auxiliary_client_ssl_verify.py @@ -40,6 +40,7 @@ def test_build_keepalive_http_client_forwards_verify_context(clean_tls_env): def test_resolve_aux_verify_ssl_verify_false(clean_tls_env, monkeypatch): import hermes_cli.config as cfg + import hermes_cli.config_providers as config_providers from agent import auxiliary_client monkeypatch.setattr( @@ -47,6 +48,11 @@ def test_resolve_aux_verify_ssl_verify_false(clean_tls_env, monkeypatch): "get_custom_provider_tls_settings", lambda *a, **k: {"ssl_verify": False}, ) + monkeypatch.setattr( + config_providers, + "get_custom_provider_tls_settings", + lambda *a, **k: {"ssl_verify": False}, + ) assert auxiliary_client._resolve_aux_verify("https://ollama.example.com/v1") is False diff --git a/tests/agent/test_credential_pool_nous_refresh_stampede.py b/tests/agent/test_credential_pool_nous_refresh_stampede.py index 34a3128e69..3b14ba5265 100644 --- a/tests/agent/test_credential_pool_nous_refresh_stampede.py +++ b/tests/agent/test_credential_pool_nous_refresh_stampede.py @@ -20,6 +20,7 @@ import json import logging import hermes_cli.auth as auth_mod +import hermes_cli.auth_nous as auth_nous from agent.credential_pool import CredentialPool, PooledCredential from tests.hermes_cli.test_auth_nous_provider import _invoke_jwt, _setup_nous_auth @@ -54,6 +55,7 @@ def test_forced_refresh_adopts_peer_rotation_instead_of_reposting(tmp_path, monk } monkeypatch.setattr(auth_mod, "_refresh_access_token", _fake_refresh_access_token) + monkeypatch.setattr(auth_nous, "_refresh_access_token", _fake_refresh_access_token) creds = auth_mod.resolve_nous_runtime_credentials( force_refresh=True, stale_access_token=failed_token @@ -94,6 +96,7 @@ def test_lock_timeout_during_nous_refresh_does_not_bench_entry(monkeypatch, capl raise TimeoutError("Timed out waiting for auth store lock") monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", _busy) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", _busy) result = pool._refresh_entry_impl(entry, force=True) @@ -124,6 +127,7 @@ def test_agent_401_refresh_passes_failed_bearer_as_stale_hint(monkeypatch): return {"api_key": "fresh", "base_url": agent.base_url} monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", _fake_resolve) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", _fake_resolve) assert agent._try_refresh_nous_client_credentials(force=True) is True assert seen["force_refresh"] is True diff --git a/tests/agent/test_credential_pool_oauth_writethrough.py b/tests/agent/test_credential_pool_oauth_writethrough.py index 1ec5526403..e176a3f406 100644 --- a/tests/agent/test_credential_pool_oauth_writethrough.py +++ b/tests/agent/test_credential_pool_oauth_writethrough.py @@ -30,6 +30,7 @@ from agent.credential_pool import ( load_pool, ) from hermes_cli import auth as A +import hermes_cli.auth_codex as auth_codex def _write_store(path, store): @@ -218,6 +219,7 @@ def test_codex_pool_refresh_holds_auth_store_lock_across_post(monkeypatch, tmp_p } monkeypatch.setattr(A, "refresh_codex_oauth_pure", fake_refresh) + monkeypatch.setattr(auth_codex, "refresh_codex_oauth_pure", fake_refresh) entry = _entry( provider, diff --git a/tests/cli/test_cli_provider_resolution.py b/tests/cli/test_cli_provider_resolution.py index ab8c7a0a8a..fefd8c0b3b 100644 --- a/tests/cli/test_cli_provider_resolution.py +++ b/tests/cli/test_cli_provider_resolution.py @@ -8,6 +8,7 @@ import pytest from hermes_cli.auth import AuthError from hermes_cli import main as hermes_main +import hermes_cli.main_provider_setup as hermes_cli_main_provider_setup from hermes_cli import model_setup_flows @@ -561,6 +562,7 @@ def test_cmd_model_forwards_nous_login_tls_options(monkeypatch): monkeypatch.setattr("hermes_cli.auth.resolve_provider", lambda requested, **kwargs: "nous") monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider_id: None) monkeypatch.setattr(hermes_main, "_prompt_provider_choice", lambda choices, **kwargs: 0) + monkeypatch.setattr(hermes_cli_main_provider_setup, "_prompt_provider_choice", lambda choices, **kwargs: 0) captured = {} diff --git a/tests/cron/test_restart_safe_worker.py b/tests/cron/test_restart_safe_worker.py index 1be7e30654..68143eb910 100644 --- a/tests/cron/test_restart_safe_worker.py +++ b/tests/cron/test_restart_safe_worker.py @@ -364,6 +364,7 @@ def test_worker_delivery_queue_is_keyed_by_the_delivering_jobs_own_execution( """A nested in-process dispatch inside a worker (e.g. a script running ``hermes cron run ``) must not queue under the OUTER execution id.""" import cron.scheduler as scheduler + import cron.scheduler_delivery as scheduler_delivery queued = [] monkeypatch.setattr( @@ -377,6 +378,11 @@ def test_worker_delivery_queue_is_keyed_by_the_delivering_jobs_own_execution( "_resolve_delivery_targets", lambda job, for_failure=False: [{"platform": "telegram", "chat_id": "123"}], ) + monkeypatch.setattr( + scheduler_delivery, + "_resolve_delivery_targets", + lambda job, for_failure=False: [{"platform": "telegram", "chat_id": "123"}], + ) def _standalone(*_args, **_kwargs): raise RuntimeError("standalone path reached") diff --git a/tests/gateway/test_api_server_active_work_drain.py b/tests/gateway/test_api_server_active_work_drain.py index 703977e40f..d29985e006 100644 --- a/tests/gateway/test_api_server_active_work_drain.py +++ b/tests/gateway/test_api_server_active_work_drain.py @@ -523,6 +523,7 @@ class TestShutdownSettleWindow: """ import tools.process_registry as _pr import tools.terminal_tool as _tt + import tools.terminal_tool_lifecycle as terminal_tool_lifecycle runner, adapter = make_restart_runner() runner._restart_drain_timeout = 0.01 # force the drain-timeout path @@ -538,6 +539,7 @@ class TestShutdownSettleWindow: monkeypatch.setattr(_pr.process_registry, "kill_all", _spy_kill_all) monkeypatch.setattr(_tt, "cleanup_all_environments", lambda: None) + monkeypatch.setattr(terminal_tool_lifecycle, "cleanup_all_environments", lambda: None) monkeypatch.setattr(bt_lifecycle, "cleanup_all_browsers", lambda: None) with patch("gateway.status.remove_pid_file"), \ @@ -566,6 +568,7 @@ class TestShutdownSettleWindow: """ import tools.process_registry as _pr import tools.terminal_tool as _tt + import tools.terminal_tool_lifecycle as terminal_tool_lifecycle runner, adapter = make_restart_runner() runner._restart_drain_timeout = 0.01 @@ -575,6 +578,7 @@ class TestShutdownSettleWindow: monkeypatch.setattr(_pr.process_registry, "kill_all", lambda task_id=None: 0) monkeypatch.setattr(_tt, "cleanup_all_environments", lambda: None) + monkeypatch.setattr(terminal_tool_lifecycle, "cleanup_all_environments", lambda: None) monkeypatch.setattr(bt_lifecycle, "cleanup_all_browsers", lambda: None) # Accelerate the loop clock: each time() call advances 1s of virtual diff --git a/tests/gateway/test_cron_active_work_drain.py b/tests/gateway/test_cron_active_work_drain.py index ac524e268a..5fc5db9013 100644 --- a/tests/gateway/test_cron_active_work_drain.py +++ b/tests/gateway/test_cron_active_work_drain.py @@ -84,6 +84,7 @@ class TestKillToolSubprocessesMarksCronInterrupted: import cron.scheduler as sched import tools.process_registry as _pr import tools.terminal_tool as _tt + import tools.terminal_tool_lifecycle as terminal_tool_lifecycle runner, adapter = make_restart_runner() runner._restart_drain_timeout = 0.01 # force the timeout path @@ -97,6 +98,7 @@ class TestKillToolSubprocessesMarksCronInterrupted: monkeypatch.setattr(_pr.process_registry, "kill_all", lambda task_id=None: 1) monkeypatch.setattr(_tt, "cleanup_all_environments", lambda: None) + monkeypatch.setattr(terminal_tool_lifecycle, "cleanup_all_environments", lambda: None) monkeypatch.setattr(bt_lifecycle, "cleanup_all_browsers", lambda: None) marked_calls = [] diff --git a/tests/gateway/test_gateway_shutdown.py b/tests/gateway/test_gateway_shutdown.py index 41cab9217c..0edb15ce1d 100644 --- a/tests/gateway/test_gateway_shutdown.py +++ b/tests/gateway/test_gateway_shutdown.py @@ -273,8 +273,10 @@ async def test_gateway_stop_kills_tool_subprocesses_before_adapter_disconnect_on # Patch the module-level names the stop() helper imports lazily. import tools.process_registry as _pr import tools.terminal_tool as _tt + import tools.terminal_tool_lifecycle as terminal_tool_lifecycle monkeypatch.setattr(_pr.process_registry, "kill_all", _fake_kill_all) monkeypatch.setattr(_tt, "cleanup_all_environments", _fake_cleanup_envs) + monkeypatch.setattr(terminal_tool_lifecycle, "cleanup_all_environments", _fake_cleanup_envs) monkeypatch.setattr(bt_lifecycle, "cleanup_all_browsers", _fake_cleanup_browsers) adapter.disconnect = _disconnect diff --git a/tests/hermes_cli/test_approval_transport.py b/tests/hermes_cli/test_approval_transport.py index a0fde80a12..e79d10e1f6 100644 --- a/tests/hermes_cli/test_approval_transport.py +++ b/tests/hermes_cli/test_approval_transport.py @@ -295,6 +295,7 @@ def test_cli_selected_transport_replaces_builtin_prompt(monkeypatch): def test_gateway_selected_transport_does_not_require_gateway_notifier(monkeypatch): from tools import approval + import tools.approval_context as tools_approval_context manager = PluginManager() seen = [] @@ -304,6 +305,7 @@ def test_gateway_selected_transport_does_not_require_gateway_notifier(monkeypatc _configure_manual_guard(monkeypatch, approval, manager) monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: True) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: True) monkeypatch.setattr(approval, "_gateway_notify_cbs", {}) result = approval.check_all_command_guards("rm -rf /tmp/example", "local") @@ -315,6 +317,7 @@ def test_gateway_selected_transport_does_not_require_gateway_notifier(monkeypatc def test_execute_code_gateway_uses_selected_transport(monkeypatch): from tools import approval + import tools.approval_context as tools_approval_context manager = PluginManager() seen = [] @@ -323,10 +326,15 @@ def test_execute_code_gateway_uses_selected_transport(monkeypatch): ) monkeypatch.setattr(approval_context, "_get_approval_mode", lambda: "manual") monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: True) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: True) monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_cron_approval_context", lambda: False) monkeypatch.setattr( approval, "get_current_session_key", lambda *args, **kwargs: "session-a" ) + monkeypatch.setattr( + tools_approval_context, "get_current_session_key", lambda *args, **kwargs: "session-a" + ) monkeypatch.setattr(approval, "is_approved", lambda *args: False) monkeypatch.setattr(approval_prompt, "get_plugin_manager", lambda: manager) monkeypatch.setattr( @@ -539,6 +547,7 @@ def register(ctx): def test_hardline_blocks_before_selected_transport(monkeypatch): from tools import approval + import tools.approval_detection as approval_detection manager = PluginManager() calls = [] @@ -551,6 +560,11 @@ def test_hardline_blocks_before_selected_transport(monkeypatch): "detect_hardline_command", lambda command: (True, "recursive delete of root filesystem"), ) + monkeypatch.setattr( + approval_detection, + "detect_hardline_command", + lambda command: (True, "recursive delete of root filesystem"), + ) result = approval.check_all_command_guards("rm -rf /", "local") diff --git a/tests/hermes_cli/test_approvals_test.py b/tests/hermes_cli/test_approvals_test.py index d8b78358ae..c67d967b12 100644 --- a/tests/hermes_cli/test_approvals_test.py +++ b/tests/hermes_cli/test_approvals_test.py @@ -16,6 +16,7 @@ import json import pytest import tools.approval as A +import tools.approval_prompt as approval_prompt from tools import approval_context from tools import approval_detection, approval_floors from hermes_cli import approvals_test as at @@ -42,6 +43,7 @@ def isolated_approvals(monkeypatch): def _boom(*_a, **_kw): # pragma: no cover - failure path raise AssertionError("read-only tester touched a prompt/persistence path") monkeypatch.setattr(A, "prompt_dangerous_approval", _boom) + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", _boom) monkeypatch.setattr(A, "save_permanent_allowlist", _boom) monkeypatch.setattr(A, "submit_pending", _boom, raising=False) yield A diff --git a/tests/hermes_cli/test_auth_codex_quota_probe.py b/tests/hermes_cli/test_auth_codex_quota_probe.py index 44bbe41ad7..b4b5c6184b 100644 --- a/tests/hermes_cli/test_auth_codex_quota_probe.py +++ b/tests/hermes_cli/test_auth_codex_quota_probe.py @@ -15,6 +15,7 @@ from types import SimpleNamespace import pytest import hermes_cli.auth as auth_mod +import hermes_cli.auth_codex as auth_codex from hermes_cli.auth import ( AuthError, _codex_usage_probe_url, @@ -229,6 +230,9 @@ def test_resolver_recovers_when_probe_confirms_reset(tmp_path, monkeypatch): monkeypatch.setattr( auth_mod, "_probe_codex_quota_restored", lambda token, **kw: True ) + monkeypatch.setattr( + auth_codex, "_probe_codex_quota_restored", lambda token, **kw: True + ) resolved = resolve_codex_runtime_credentials() assert resolved["api_key"] == "tok-quota" @@ -270,6 +274,7 @@ def test_pool_probe_not_fired_for_non_quota_exhaustion(tmp_path, monkeypatch): return True monkeypatch.setattr(auth_mod, "_probe_codex_quota_restored", _spy) + monkeypatch.setattr(auth_codex, "_probe_codex_quota_restored", _spy) pool._available_entries(clear_expired=True, refresh=False) assert probes == [] diff --git a/tests/hermes_cli/test_auth_codex_self_heal.py b/tests/hermes_cli/test_auth_codex_self_heal.py index 2c1fd2f9b4..4c92cde2f5 100644 --- a/tests/hermes_cli/test_auth_codex_self_heal.py +++ b/tests/hermes_cli/test_auth_codex_self_heal.py @@ -15,6 +15,7 @@ import json import pytest import hermes_cli.auth as auth +import hermes_cli.auth_codex as auth_codex from hermes_cli.auth import AuthError, _refresh_codex_auth_tokens, resolve_codex_runtime_credentials STALE = {"access_token": "stale-access", "refresh_token": "stale-refresh"} @@ -38,8 +39,11 @@ def test_self_heals_on_stale_refresh_token(monkeypatch): ) monkeypatch.setattr(auth, "refresh_codex_oauth_pure", _rejected) + monkeypatch.setattr(auth_codex, "refresh_codex_oauth_pure", _rejected) monkeypatch.setattr(auth, "_import_codex_cli_tokens", lambda: dict(fresh)) + monkeypatch.setattr(auth_codex, "_import_codex_cli_tokens", lambda: dict(fresh)) monkeypatch.setattr(auth, "_save_codex_tokens", lambda t, *a, **k: saved.update(t)) + monkeypatch.setattr(auth_codex, "_save_codex_tokens", lambda t, *a, **k: saved.update(t)) out = _refresh_codex_auth_tokens(STALE, 20.0) diff --git a/tests/hermes_cli/test_auth_loopback_ssh_hint.py b/tests/hermes_cli/test_auth_loopback_ssh_hint.py index d54f10b91d..268d72bac4 100644 --- a/tests/hermes_cli/test_auth_loopback_ssh_hint.py +++ b/tests/hermes_cli/test_auth_loopback_ssh_hint.py @@ -16,6 +16,7 @@ import socket from hermes_cli import auth as auth_mod +import hermes_cli.auth_device_flow as auth_device_flow def _cap(fn): @@ -27,6 +28,7 @@ def _cap(fn): def test_loopback_ssh_hint_silent_when_not_remote(monkeypatch): monkeypatch.setattr(auth_mod, "_is_remote_session", lambda: False) + monkeypatch.setattr(auth_device_flow, "_is_remote_session", lambda: False) out = _cap(lambda: auth_mod._print_loopback_ssh_hint( "http://127.0.0.1:43827/spotify/callback", docs_url=auth_mod.SPOTIFY_DOCS_URL )) @@ -36,6 +38,7 @@ def test_loopback_ssh_hint_silent_when_not_remote(monkeypatch): def test_loopback_ssh_hint_has_visual_header(monkeypatch): """The hint should print a divider and header so it stands out in noisy output.""" monkeypatch.setattr(auth_mod, "_is_remote_session", lambda: True) + monkeypatch.setattr(auth_device_flow, "_is_remote_session", lambda: True) out = _cap(lambda: auth_mod._print_loopback_ssh_hint( "http://127.0.0.1:43827/callback" )) diff --git a/tests/hermes_cli/test_auth_nous_provider.py b/tests/hermes_cli/test_auth_nous_provider.py index 54b111a299..8add56de1d 100644 --- a/tests/hermes_cli/test_auth_nous_provider.py +++ b/tests/hermes_cli/test_auth_nous_provider.py @@ -184,6 +184,7 @@ def test_resolve_nous_runtime_credentials_invoke_jwt_is_idempotent( monkeypatch, ): import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous hermes_home = tmp_path / "hermes" hermes_home.mkdir(parents=True, exist_ok=True) @@ -232,11 +233,17 @@ def test_resolve_nous_runtime_credentials_invoke_jwt_is_idempotent( sync_calls = [] monkeypatch.setattr(auth_mod, "_write_shared_nous_state", _unexpected_shared_write) + monkeypatch.setattr(auth_nous, "_write_shared_nous_state", _unexpected_shared_write) monkeypatch.setattr( auth_mod, "_sync_nous_pool_from_auth_store", lambda: sync_calls.append(True), ) + monkeypatch.setattr( + auth_nous, + "_sync_nous_pool_from_auth_store", + lambda: sync_calls.append(True), + ) creds = auth_mod.resolve_nous_runtime_credentials() @@ -351,6 +358,7 @@ def test_nous_inference_auth_logs_do_not_include_secret_values( caplog, ): import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous hermes_home = tmp_path / "hermes" token = _invoke_jwt(seconds=3600) @@ -377,6 +385,7 @@ def test_nous_inference_auth_logs_do_not_include_secret_values( } monkeypatch.setattr(auth_mod, "_refresh_access_token", _fake_refresh_access_token) + monkeypatch.setattr(auth_nous, "_refresh_access_token", _fake_refresh_access_token) caplog.set_level(logging.DEBUG, logger="hermes_cli.auth") auth_mod.resolve_nous_runtime_credentials( @@ -499,6 +508,7 @@ class TestLoginNousSkipKeepsCurrent: def _patch_login_internals(self, monkeypatch, *, prompt_returns): """Patch OAuth + model-list + prompt so _login_nous doesn't hit network.""" import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous import hermes_cli.models as models_mod from hermes_cli import models_pricing import hermes_cli.nous_subscription as ns @@ -515,6 +525,10 @@ class TestLoginNousSkipKeepsCurrent: auth_mod, "_nous_device_code_login", lambda **kwargs: dict(fake_auth_state), ) + monkeypatch.setattr( + auth_nous, "_nous_device_code_login", + lambda **kwargs: dict(fake_auth_state), + ) monkeypatch.setattr( auth_mod, "_prompt_model_selection", lambda *a, **kw: prompt_returns, @@ -963,6 +977,7 @@ def test_try_import_shared_rehydrates_on_success(shared_store_env, monkeypatch): every field persist_nous_credentials() needs. """ from hermes_cli import auth as auth_mod + import hermes_cli.auth_nous as auth_nous auth_mod._write_shared_nous_state(_full_state_fixture()) fresh_jwt = _invoke_jwt(seconds=7200) @@ -979,6 +994,7 @@ def test_try_import_shared_rehydrates_on_success(shared_store_env, monkeypatch): } monkeypatch.setattr(auth_mod, "refresh_nous_oauth_from_state", _fake_refresh) + monkeypatch.setattr(auth_nous, "refresh_nous_oauth_from_state", _fake_refresh) result = auth_mod._try_import_shared_nous_state() @@ -1032,6 +1048,7 @@ class TestStalePortalBaseUrlMigration: ): """An allowlisted production host is still unsafe over plain HTTP.""" from hermes_cli import auth as auth_mod + import hermes_cli.auth_nous as auth_nous hermes_home = tmp_path / "hermes" monkeypatch.setenv("HERMES_HOME", str(hermes_home)) @@ -1066,6 +1083,9 @@ class TestStalePortalBaseUrlMigration: monkeypatch.setattr( auth_mod, "_refresh_access_token", _fake_refresh_access_token ) + monkeypatch.setattr( + auth_nous, "_refresh_access_token", _fake_refresh_access_token + ) auth_mod.resolve_nous_runtime_credentials() assert refresh_calls == [auth_mod.DEFAULT_NOUS_PORTAL_URL] diff --git a/tests/hermes_cli/test_auth_store_windows_encoding.py b/tests/hermes_cli/test_auth_store_windows_encoding.py index 0f45be6b45..12792f0449 100644 --- a/tests/hermes_cli/test_auth_store_windows_encoding.py +++ b/tests/hermes_cli/test_auth_store_windows_encoding.py @@ -22,6 +22,7 @@ from unittest import mock import pytest import hermes_cli.auth as auth +import hermes_cli.auth_codex as auth_codex # --- helpers --------------------------------------------------------------- @@ -170,6 +171,7 @@ class TestExplicitEncodingPassed: monkeypatch.setenv("CODEX_HOME", str(codex_home)) # Bypass the JWT-expiry check so a fake token doesn't short-circuit. monkeypatch.setattr(auth, "_codex_access_token_is_expiring", lambda *a, **k: False) + monkeypatch.setattr(auth_codex, "_codex_access_token_is_expiring", lambda *a, **k: False) with mock.patch.object(Path, "read_text", wraps=Path.read_text) as spy: auth._import_codex_cli_tokens() diff --git a/tests/hermes_cli/test_aux_config.py b/tests/hermes_cli/test_aux_config.py index fb2b0f7668..f26baa47fa 100644 --- a/tests/hermes_cli/test_aux_config.py +++ b/tests/hermes_cli/test_aux_config.py @@ -172,6 +172,7 @@ def test_leave_unchanged_replaces_cancel_label(tmp_path, monkeypatch): (tmp_path / ".hermes").mkdir(exist_ok=True) from hermes_cli import main as main_mod + import hermes_cli.main_provider_setup as hermes_cli_main_provider_setup captured: list[list[str]] = [] @@ -184,6 +185,7 @@ def test_leave_unchanged_replaces_cancel_label(tmp_path, monkeypatch): raise AssertionError("Leave unchanged not in provider list") monkeypatch.setattr(main_mod, "_prompt_provider_choice", fake_prompt) + monkeypatch.setattr(hermes_cli_main_provider_setup, "_prompt_provider_choice", fake_prompt) main_mod.select_provider_and_model() diff --git a/tests/hermes_cli/test_billing_scope_stepup.py b/tests/hermes_cli/test_billing_scope_stepup.py index 3841921393..c1520776f0 100644 --- a/tests/hermes_cli/test_billing_scope_stepup.py +++ b/tests/hermes_cli/test_billing_scope_stepup.py @@ -5,6 +5,7 @@ from __future__ import annotations import pytest import hermes_cli.auth as auth +import hermes_cli.auth_nous as auth_nous from hermes_cli.auth import ( NOUS_BILLING_MANAGE_SCOPE, nous_token_has_billing_scope, @@ -54,7 +55,9 @@ def _stub_persist(monkeypatch): monkeypatch.setattr(auth, "_save_provider_state", lambda *a, **kw: None) monkeypatch.setattr(auth, "_save_auth_store", lambda *a, **kw: "auth.json") monkeypatch.setattr(auth, "_write_shared_nous_state", lambda *a, **kw: None) + monkeypatch.setattr(auth_nous, "_write_shared_nous_state", lambda *a, **kw: None) monkeypatch.setattr(auth, "_sync_nous_pool_from_auth_store", lambda: None) + monkeypatch.setattr(auth_nous, "_sync_nous_pool_from_auth_store", lambda: None) class _NullCtx: @@ -84,6 +87,7 @@ def test_step_up_requests_billing_scope_and_reuses_prior_urls(monkeypatch, _stub return {"scope": "inference:invoke tool:invoke billing:manage", "access_token": "t"} monkeypatch.setattr(auth, "_nous_device_code_login", _fake_login) + monkeypatch.setattr(auth_nous, "_nous_device_code_login", _fake_login) granted = step_up_nous_billing_scope() assert granted is True diff --git a/tests/hermes_cli/test_checkout_mutation_guards.py b/tests/hermes_cli/test_checkout_mutation_guards.py index 86090ee73e..5c40d2513d 100644 --- a/tests/hermes_cli/test_checkout_mutation_guards.py +++ b/tests/hermes_cli/test_checkout_mutation_guards.py @@ -18,6 +18,7 @@ from __future__ import annotations from pathlib import Path import hermes_cli.main as main_mod +import hermes_cli.main_install_repair as hermes_cli_main_install_repair from hermes_cli import main_install_repair from hermes_cli import update_cmd from hermes_cli import _early_recovery as er @@ -107,4 +108,5 @@ class TestLaunchRecovery: raise AssertionError("launch recovery ran against the live checkout") monkeypatch.setattr(main_mod, "_update_marker_path", _boom) + monkeypatch.setattr(hermes_cli_main_install_repair, "_update_marker_path", _boom) main_mod._recover_from_interrupted_install() diff --git a/tests/hermes_cli/test_computer_use_cli.py b/tests/hermes_cli/test_computer_use_cli.py index 4766152077..a3e17aade1 100644 --- a/tests/hermes_cli/test_computer_use_cli.py +++ b/tests/hermes_cli/test_computer_use_cli.py @@ -53,6 +53,7 @@ def test_computer_use_status_returns_zero_for_compatible_driver( monkeypatch: pytest.MonkeyPatch, ) -> None: from hermes_cli import tools_config + import hermes_cli.tools_config_cua as tools_config_cua driver = r"C:\Users\tester\.local\bin\cua-driver.exe" monkeypatch.delenv("HERMES_CUA_DRIVER_CMD", raising=False) @@ -62,6 +63,11 @@ def test_computer_use_status_returns_zero_for_compatible_driver( "_cua_driver_contract_status", lambda _binary=None: {"ready": True}, ) + monkeypatch.setattr( + tools_config_cua, + "_cua_driver_contract_status", + lambda _binary=None: {"ready": True}, + ) monkeypatch.setattr( cua_backend_driver, "cua_driver_update_check", @@ -87,6 +93,7 @@ def test_computer_use_status_returns_nonzero_for_incompatible_standard_driver( capsys: pytest.CaptureFixture[str], ) -> None: from hermes_cli import tools_config + import hermes_cli.tools_config_cua as tools_config_cua driver = r"C:\Users\tester\.local\bin\cua-driver.exe" monkeypatch.delenv("HERMES_CUA_DRIVER_CMD", raising=False) @@ -99,6 +106,14 @@ def test_computer_use_status_returns_nonzero_for_incompatible_standard_driver( "reason": "required runtime features are missing", }, ) + monkeypatch.setattr( + tools_config_cua, + "_cua_driver_contract_status", + lambda _binary=None: { + "ready": False, + "reason": "required runtime features are missing", + }, + ) assert _invoke(monkeypatch, "status") == 1 output = capsys.readouterr().out @@ -111,6 +126,7 @@ def test_computer_use_status_returns_nonzero_for_incompatible_custom_driver( capsys: pytest.CaptureFixture[str], ) -> None: from hermes_cli import tools_config + import hermes_cli.tools_config_cua as tools_config_cua driver = r"C:\custom\cmd.exe" monkeypatch.setenv("HERMES_CUA_DRIVER_CMD", driver) @@ -120,6 +136,11 @@ def test_computer_use_status_returns_nonzero_for_incompatible_custom_driver( "_cua_driver_contract_status", lambda _binary=None: {"ready": False, "reason": "manifest is invalid"}, ) + monkeypatch.setattr( + tools_config_cua, + "_cua_driver_contract_status", + lambda _binary=None: {"ready": False, "reason": "manifest is invalid"}, + ) assert _invoke(monkeypatch, "status") == 1 output = capsys.readouterr().out @@ -134,6 +155,7 @@ def test_computer_use_install_checks_resulting_runtime_contract( expected: int, ) -> None: from hermes_cli import tools_config + import hermes_cli.tools_config_cua as tools_config_cua install = Mock(return_value=True) monkeypatch.setattr(tools_config, "install_cua_driver", install) @@ -142,6 +164,11 @@ def test_computer_use_install_checks_resulting_runtime_contract( "_cua_driver_contract_status", lambda: {"ready": ready}, ) + monkeypatch.setattr( + tools_config_cua, + "_cua_driver_contract_status", + lambda: {"ready": ready}, + ) assert _invoke(monkeypatch, "install") == expected install.assert_called_once_with(upgrade=False) @@ -151,6 +178,7 @@ def test_computer_use_install_returns_nonzero_for_unrepairable_custom_override( monkeypatch: pytest.MonkeyPatch, ) -> None: from hermes_cli import tools_config + import hermes_cli.tools_config_cua as tools_config_cua driver = r"C:\custom\cmd.exe" monkeypatch.setenv("HERMES_CUA_DRIVER_CMD", driver) @@ -158,6 +186,7 @@ def test_computer_use_install_returns_nonzero_for_unrepairable_custom_override( contract = Mock(side_effect=AssertionError("failed install must short-circuit")) monkeypatch.setattr(tools_config, "install_cua_driver", install) monkeypatch.setattr(tools_config, "_cua_driver_contract_status", contract) + monkeypatch.setattr(tools_config_cua, "_cua_driver_contract_status", contract) assert _invoke(monkeypatch, "install") == 1 install.assert_called_once_with(upgrade=False) diff --git a/tests/hermes_cli/test_imagegen_managed_gateway.py b/tests/hermes_cli/test_imagegen_managed_gateway.py index e9ddc67018..2ff4ee655b 100644 --- a/tests/hermes_cli/test_imagegen_managed_gateway.py +++ b/tests/hermes_cli/test_imagegen_managed_gateway.py @@ -80,6 +80,7 @@ def test_image_and_video_selectors_share_the_selection_contract(monkeypatch): def _quiet_reconfigure(monkeypatch): """Silence prints + model pickers for _reconfigure_provider paths.""" import hermes_cli.tools_config as tc + import hermes_cli.tools_config_post_setup as tools_config_post_setup import hermes_cli.tools_config_providers as tcp monkeypatch.setattr(tcp, "_print_success", lambda *a, **k: None) @@ -88,6 +89,7 @@ def _quiet_reconfigure(monkeypatch): monkeypatch.setattr(tcp, "_configure_imagegen_model", lambda *a, **k: None) # _configure_provider resolves the post-setup hook lazily from tools_config. monkeypatch.setattr(tc, "_run_post_setup", lambda *a, **k: None, raising=False) + monkeypatch.setattr(tools_config_post_setup, "_run_post_setup", lambda *a, **k: None, raising=False) # Managed rows gate on live Portal auth — stub it green. import hermes_cli.nous_subscription as ns diff --git a/tests/hermes_cli/test_lazy_refresh_venv_repair.py b/tests/hermes_cli/test_lazy_refresh_venv_repair.py index e643691153..9885711516 100644 --- a/tests/hermes_cli/test_lazy_refresh_venv_repair.py +++ b/tests/hermes_cli/test_lazy_refresh_venv_repair.py @@ -8,6 +8,7 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch import hermes_cli.main as m +import hermes_cli.main_install_repair as hermes_cli_main_install_repair from hermes_cli import main_install_repair from hermes_cli import update_cmd import pytest @@ -23,6 +24,9 @@ def test_detect_returns_none_when_probe_subprocess_fails(tmp_path, monkeypatch): monkeypatch.setattr( m, "_resolve_install_target_python", lambda *a, **k: python ) + monkeypatch.setattr( + hermes_cli_main_install_repair, "_resolve_install_target_python", lambda *a, **k: python + ) monkeypatch.setattr( m.subprocess, "run", @@ -155,6 +159,11 @@ def test_restore_active_tool_dependencies_uses_static_allowlist(monkeypatch): "_run_package_only_install", lambda cmd, *, env=None: calls.append((cmd, env)), ) + monkeypatch.setattr( + hermes_cli_main_install_repair, + "_run_package_only_install", + lambda cmd, *, env=None: calls.append((cmd, env)), + ) env = {"VIRTUAL_ENV": "/tmp/venv"} m._restore_active_tool_dependencies( @@ -202,6 +211,7 @@ def test_cmd_update_captures_and_propagates_pre_rebuild_snapshot( m, "_capture_active_tool_dependencies", lambda: tool_snapshot.copy() ) monkeypatch.setattr(m, "_is_windows", lambda: False) + monkeypatch.setattr(hermes_cli_main_install_repair, "_is_windows", lambda: False) monkeypatch.setattr(m, "_run_pre_update_backup", lambda args: None) monkeypatch.setattr(m, "_pause_windows_gateways_for_update", lambda: None) monkeypatch.setattr(m, "_resume_windows_gateways_after_update", lambda state: None) diff --git a/tests/hermes_cli/test_list_picker_providers.py b/tests/hermes_cli/test_list_picker_providers.py index ab15213cd4..21581fd204 100644 --- a/tests/hermes_cli/test_list_picker_providers.py +++ b/tests/hermes_cli/test_list_picker_providers.py @@ -17,6 +17,7 @@ network or auth state is required. import pytest from hermes_cli import model_switch +import hermes_cli.model_switch_providers as hermes_cli_model_switch_providers from hermes_cli import model_switch_providers @@ -73,6 +74,7 @@ def test_passthrough_kwargs_to_base(monkeypatch): return [] monkeypatch.setattr(model_switch, "list_authenticated_providers", _capture) + monkeypatch.setattr(hermes_cli_model_switch_providers, "list_authenticated_providers", _capture) monkeypatch.setattr("hermes_cli.models.fetch_openrouter_models", lambda *a, **kw: []) @@ -159,6 +161,7 @@ def _stub_kimi_discovery(monkeypatch, *, canonical): """ import agent.models_dev as md import hermes_cli.models as hm + import hermes_cli.models_catalog_static as hermes_cli_models_catalog_static from hermes_cli import models_catalog_static kimi_map = { @@ -181,6 +184,7 @@ def _stub_kimi_discovery(monkeypatch, *, canonical): monkeypatch.setattr(md, "get_provider_info", lambda _pid: _PInfo()) monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) monkeypatch.setattr(hm, "CANONICAL_PROVIDERS", canonical) + monkeypatch.setattr(hermes_cli_models_catalog_static, "CANONICAL_PROVIDERS", canonical) monkeypatch.setattr(hm, "cached_provider_model_ids", lambda *a, **k: ["kimi-k2.6", "kimi-k2.5"]) monkeypatch.setattr(hm, "clear_provider_models_cache", lambda *a, **k: None) diff --git a/tests/hermes_cli/test_nous_inference_url_validation.py b/tests/hermes_cli/test_nous_inference_url_validation.py index a07ef9efbf..7a20149ab8 100644 --- a/tests/hermes_cli/test_nous_inference_url_validation.py +++ b/tests/hermes_cli/test_nous_inference_url_validation.py @@ -173,6 +173,7 @@ class TestHealsPoisonedStoredValue: def test_refresh_resets_rejected_url_to_default(self, monkeypatch): import hermes_cli.auth as auth + import hermes_cli.auth_nous as hermes_cli_auth_nous poisoned = "https://stg-inference-api.nousresearch.com/v1" state = { @@ -186,6 +187,7 @@ class TestHealsPoisonedStoredValue: # Force the refresh branch and return another rejected (staging) URL, # exercising the validator-returns-None heal path. monkeypatch.setattr(auth, "_nous_invoke_jwt_status", lambda *a, **k: "needs_refresh") + monkeypatch.setattr(hermes_cli_auth_nous, "_nous_invoke_jwt_status", lambda *a, **k: "needs_refresh") monkeypatch.setattr( auth, "_refresh_access_token", @@ -196,9 +198,21 @@ class TestHealsPoisonedStoredValue: "inference_base_url": poisoned, # Portal still hands back staging }, ) + monkeypatch.setattr( + hermes_cli_auth_nous, + "_refresh_access_token", + lambda **k: { + "access_token": "newtok", + "refresh_token": "newrtok", + "expires_in": 3600, + "inference_base_url": poisoned, # Portal still hands back staging + }, + ) # Skip the JWT usability assertions (orthogonal to URL healing). monkeypatch.setattr(auth, "_assert_nous_inference_jwt_usable", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_assert_nous_inference_jwt_usable", lambda *a, **k: None) monkeypatch.setattr(auth, "_select_nous_invoke_jwt", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_select_nous_invoke_jwt", lambda *a, **k: None) result = auth.refresh_nous_oauth_from_state(state, force_refresh=True) @@ -226,10 +240,12 @@ class TestEnvOverrideWins: STAGING = "https://stg-inference-api.nousresearch.com/v1" def _patch_no_refresh(self, monkeypatch, auth, state): + import hermes_cli.auth_nous as hermes_cli_auth_nous import contextlib # No refresh fires: the stored access token is a usable invoke JWT. monkeypatch.setattr(auth, "_nous_invoke_jwt_status", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_nous_invoke_jwt_status", lambda *a, **k: None) monkeypatch.setattr( auth, "_auth_store_lock", lambda *a, **k: contextlib.nullcontext() ) @@ -244,10 +260,14 @@ class TestEnvOverrideWins: monkeypatch.setattr(auth, "_save_provider_state_to_source", lambda *a, **k: None) monkeypatch.setattr(auth, "_save_auth_store", lambda *a, **k: None) monkeypatch.setattr(auth, "_write_shared_nous_state", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_write_shared_nous_state", lambda *a, **k: None) monkeypatch.setattr(auth, "_sync_nous_pool_from_auth_store", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_sync_nous_pool_from_auth_store", lambda *a, **k: None) monkeypatch.setattr(auth, "_resolve_verify", lambda *a, **k: True) monkeypatch.setattr(auth, "_assert_nous_inference_jwt_usable", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_assert_nous_inference_jwt_usable", lambda *a, **k: None) monkeypatch.setattr(auth, "_select_nous_invoke_jwt", lambda *a, **k: None) + monkeypatch.setattr(hermes_cli_auth_nous, "_select_nous_invoke_jwt", lambda *a, **k: None) def _base_state(self, auth, stored): return { diff --git a/tests/hermes_cli/test_nous_policy_surfaces.py b/tests/hermes_cli/test_nous_policy_surfaces.py index 689bce0532..d15f8e0333 100644 --- a/tests/hermes_cli/test_nous_policy_surfaces.py +++ b/tests/hermes_cli/test_nous_policy_surfaces.py @@ -34,6 +34,7 @@ class TestLoginNous: def _run(self, monkeypatch, tmp_path): import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous import hermes_cli.nous_subscription as ns seen: dict = {} @@ -50,6 +51,18 @@ class TestLoginNous: "token_expires_at": 9999999999, }, ) + monkeypatch.setattr( + auth_nous, + "_nous_device_code_login", + lambda **_k: { + "access_token": "tok", + "agent_key": "key", + "inference_base_url": "https://inference.example.com", + "portal_base_url": "https://portal.example.com", + "refresh_token": "r", + "token_expires_at": 9999999999, + }, + ) monkeypatch.setattr(models_mod, "get_curated_nous_model_ids", lambda: list(CURATED)) monkeypatch.setattr(models_pricing, "get_pricing_for_provider", lambda _p: {}) monkeypatch.setattr(models_mod, "check_nous_free_tier", lambda **_k: None) diff --git a/tests/hermes_cli/test_nous_portal_staging_allowlist.py b/tests/hermes_cli/test_nous_portal_staging_allowlist.py index 34e02049dd..d3fc7781b7 100644 --- a/tests/hermes_cli/test_nous_portal_staging_allowlist.py +++ b/tests/hermes_cli/test_nous_portal_staging_allowlist.py @@ -85,6 +85,7 @@ class TestResolveAccessTokenEnvOverrideWins: return auth_file def _run_and_capture(self, monkeypatch, auth): + import hermes_cli.auth_nous as auth_nous seen_portal_urls = [] # The resolve memo is module-level state; clear it so each test's @@ -101,6 +102,7 @@ class TestResolveAccessTokenEnvOverrideWins: } monkeypatch.setattr(auth, "_refresh_access_token", _fake_refresh) + monkeypatch.setattr(auth_nous, "_refresh_access_token", _fake_refresh) caplog_records = [] logger = logging.getLogger("hermes_cli.auth") diff --git a/tests/hermes_cli/test_nous_session_validity.py b/tests/hermes_cli/test_nous_session_validity.py index 579c2e7fb3..ff09cb77d8 100644 --- a/tests/hermes_cli/test_nous_session_validity.py +++ b/tests/hermes_cli/test_nous_session_validity.py @@ -5,6 +5,7 @@ import json import time import hermes_cli.auth as auth +import hermes_cli.auth_nous as auth_nous from hermes_cli.auth import ( NOUS_SESSION_TERMINAL, NOUS_SESSION_UNKNOWN, @@ -44,6 +45,11 @@ def _block_live_auth(monkeypatch): "resolve_nous_runtime_credentials", _fail_if_live_auth_is_used, ) + monkeypatch.setattr( + auth_nous, + "resolve_nous_runtime_credentials", + _fail_if_live_auth_is_used, + ) diff --git a/tests/hermes_cli/test_relay_shared_metrics_runtime.py b/tests/hermes_cli/test_relay_shared_metrics_runtime.py index 1e3f591743..1eecaaed73 100644 --- a/tests/hermes_cli/test_relay_shared_metrics_runtime.py +++ b/tests/hermes_cli/test_relay_shared_metrics_runtime.py @@ -745,6 +745,8 @@ def test_real_binding_correlates_plugin_approval_denial_to_tool_metric( ): from hermes_cli.observability.shared_metrics import SharedMetricsStore from tools import approval + import tools.approval_prompt as approval_prompt + import tools.approval_context as approval_context assert real_binding_runtime._native is not None base = { @@ -765,9 +767,12 @@ def test_real_binding_correlates_plugin_approval_denial_to_tool_metric( monkeypatch.setattr(approval, "is_current_session_yolo_enabled", lambda: False) monkeypatch.setattr(approval, "is_approved", lambda *args: False) monkeypatch.setattr(approval, "get_current_session_key", lambda: "session-key") + monkeypatch.setattr(approval_context, "get_current_session_key", lambda: "session-key") monkeypatch.setattr(approval, "_is_interactive_cli", lambda: True) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "prompt_dangerous_approval", lambda *args, **kwargs: "deny") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", lambda *args, **kwargs: "deny") lifecycle.invoke_hook("on_session_start", **base) lifecycle.invoke_hook("pre_llm_call", **base, messages=["sensitive-prompt"]) diff --git a/tests/hermes_cli/test_serve_mcp_discovery_after_bind.py b/tests/hermes_cli/test_serve_mcp_discovery_after_bind.py index 0ae23f019e..d53d692579 100644 --- a/tests/hermes_cli/test_serve_mcp_discovery_after_bind.py +++ b/tests/hermes_cli/test_serve_mcp_discovery_after_bind.py @@ -12,6 +12,7 @@ import threading import hermes_cli.mcp_startup as mcp_startup import hermes_cli.web_server as web_server +import hermes_cli.web_server_lifecycle as web_server_lifecycle from tests.hermes_cli.test_dashboard_auth_gate import _stub_uvicorn_run @@ -30,6 +31,7 @@ def test_desktop_serve_arms_mcp_discovery_only_after_ready_sentinel(monkeypatch) lambda *, logger, thread_name: order.append("discovery:" + thread_name), ) monkeypatch.setattr(web_server, "_write_machine_sentinel_line", lambda line: order.append("sentinel")) + monkeypatch.setattr(web_server_lifecycle, "_write_machine_sentinel_line", lambda line: order.append("sentinel")) _stub_uvicorn_run(monkeypatch) web_server.start_server( diff --git a/tests/hermes_cli/test_serve_runtime_inventory.py b/tests/hermes_cli/test_serve_runtime_inventory.py index 40481462fc..553ba4d410 100644 --- a/tests/hermes_cli/test_serve_runtime_inventory.py +++ b/tests/hermes_cli/test_serve_runtime_inventory.py @@ -16,6 +16,8 @@ from unittest.mock import patch # noqa: F401 - kept for parity with siblings import hermes_cli.update_cmd as update_cmd import hermes_cli.update_inventory as update_inventory from hermes_cli import main as cli_main +import hermes_cli.main_install_repair as main_install_repair +import hermes_cli.main_dashboard as main_dashboard def _ledger_entry(**over): @@ -142,6 +144,7 @@ def test_ledger_manual_serve_holders_filters_correctly(monkeypatch): def test_serve_relaunch_commands_built_from_structured_identity(monkeypatch): monkeypatch.setattr(cli_main, "_venv_scripts_dir", lambda: None) + monkeypatch.setattr(main_install_repair, "_venv_scripts_dir", lambda: None) entries = [ _ledger_entry(), # default profile _ledger_entry(pid=5000, profile="work", port=9200, host=""), @@ -160,7 +163,11 @@ def test_relaunch_stopped_serves_is_idempotent(monkeypatch): monkeypatch.setattr( cli_main, "_respawn_dashboard_processes", lambda cmds: calls.append(cmds) or [] ) + monkeypatch.setattr( + main_dashboard, "_respawn_dashboard_processes", lambda cmds: calls.append(cmds) or [] + ) monkeypatch.setattr(cli_main, "_venv_scripts_dir", lambda: None) + monkeypatch.setattr(main_install_repair, "_venv_scripts_dir", lambda: None) token = {"pending": True, "entries": [_ledger_entry()]} update_cmd._relaunch_stopped_serves(token) @@ -175,6 +182,9 @@ def test_relaunch_stopped_serves_untriggered_token_noop(monkeypatch): monkeypatch.setattr( cli_main, "_respawn_dashboard_processes", lambda cmds: calls.append(cmds) or [] ) + monkeypatch.setattr( + main_dashboard, "_respawn_dashboard_processes", lambda cmds: calls.append(cmds) or [] + ) update_cmd._relaunch_stopped_serves({"pending": False, "entries": [_ledger_entry()]}) assert calls == [] diff --git a/tests/hermes_cli/test_shim_fail_closed_windows_live.py b/tests/hermes_cli/test_shim_fail_closed_windows_live.py index 9c109a8f9f..73d9446a99 100644 --- a/tests/hermes_cli/test_shim_fail_closed_windows_live.py +++ b/tests/hermes_cli/test_shim_fail_closed_windows_live.py @@ -75,6 +75,7 @@ def test_locked_shim_really_cannot_be_renamed(held_shim): def test_strict_quarantine_refuses_against_real_lock(held_shim, monkeypatch): import hermes_cli.main as cli_main + import hermes_cli.main_install_repair as hermes_cli_main_install_repair scripts, _shim = held_shim install_ran: list = [] @@ -83,6 +84,11 @@ def test_strict_quarantine_refuses_against_real_lock(held_shim, monkeypatch): "_run_install_with_heartbeat", lambda cmd, env=None: install_ran.append(cmd), ) + monkeypatch.setattr( + hermes_cli_main_install_repair, + "_run_install_with_heartbeat", + lambda cmd, env=None: install_ran.append(cmd), + ) with pytest.raises(main_install_repair.ShimQuarantineError) as exc_info: main_install_repair._run_quarantined_install( @@ -114,6 +120,7 @@ def test_recovery_installer_refuses_against_real_lock(held_shim, monkeypatch): def test_release_then_strict_quarantine_succeeds(tmp_path, monkeypatch): """After the holder exits, the same strict path proceeds normally.""" import hermes_cli.main as cli_main + import hermes_cli.main_install_repair as hermes_cli_main_install_repair scripts = tmp_path / "venv" / "Scripts" scripts.mkdir(parents=True) @@ -135,6 +142,11 @@ def test_release_then_strict_quarantine_succeeds(tmp_path, monkeypatch): "_run_install_with_heartbeat", lambda cmd, env=None: install_ran.append(cmd), ) + monkeypatch.setattr( + hermes_cli_main_install_repair, + "_run_install_with_heartbeat", + lambda cmd, env=None: install_ran.append(cmd), + ) main_install_repair._run_quarantined_install( ["fake"], scripts_dir=scripts, strict_quarantine=True ) diff --git a/tests/hermes_cli/test_sibling_config_migration.py b/tests/hermes_cli/test_sibling_config_migration.py index 3716dc6a0c..a9847ba2d7 100644 --- a/tests/hermes_cli/test_sibling_config_migration.py +++ b/tests/hermes_cli/test_sibling_config_migration.py @@ -14,6 +14,7 @@ import yaml from pathlib import Path import hermes_cli.update_cmd as update_cmd +import hermes_cli.update_cmd_config as update_cmd_config def _write_profile(root: Path, name: str, version: int) -> Path: @@ -44,6 +45,9 @@ def _setup(monkeypatch, tmp_path, active_home: Path): monkeypatch.setattr( update_cmd, "_reload_config_modules", lambda: None ) # module reload is orthogonal here; the real one re-imports from disk + monkeypatch.setattr( + update_cmd_config, "_reload_config_modules", lambda: None + ) # module reload is orthogonal here; the real one re-imports from disk def test_sibling_behind_is_migrated_on_disk(monkeypatch, tmp_path): diff --git a/tests/hermes_cli/test_spotify_auth.py b/tests/hermes_cli/test_spotify_auth.py index d9e66c66ce..07b86c6f4f 100644 --- a/tests/hermes_cli/test_spotify_auth.py +++ b/tests/hermes_cli/test_spotify_auth.py @@ -5,6 +5,7 @@ from types import SimpleNamespace import pytest from hermes_cli import auth as auth_mod +import hermes_cli.auth_spotify as auth_spotify from hermes_cli.auth import AuthError, resolve_spotify_runtime_credentials @@ -46,6 +47,15 @@ def test_resolve_spotify_runtime_credentials_refreshes_without_changing_active_p "expires_at": "2099-01-01T00:00:00+00:00", }, ) + monkeypatch.setattr( + auth_spotify, + "_refresh_spotify_oauth_state", + lambda state, timeout_seconds=20.0: { + **state, + "access_token": "fresh-token", + "expires_at": "2099-01-01T00:00:00+00:00", + }, + ) creds = auth_mod.resolve_spotify_runtime_credentials() @@ -131,6 +141,7 @@ def test_resolve_credentials_quarantines_dead_tokens_on_terminal_refresh_failure ) monkeypatch.setattr(auth_mod, "_refresh_spotify_oauth_state", _terminal_refresh) + monkeypatch.setattr(auth_spotify, "_refresh_spotify_oauth_state", _terminal_refresh) with pytest.raises(AuthError) as exc_info: resolve_spotify_runtime_credentials(force_refresh=True) diff --git a/tests/hermes_cli/test_subscription_cli.py b/tests/hermes_cli/test_subscription_cli.py index 5a16a8ce04..b5098f5c6f 100644 --- a/tests/hermes_cli/test_subscription_cli.py +++ b/tests/hermes_cli/test_subscription_cli.py @@ -132,8 +132,10 @@ def test_open_url_in_browser_refuses_remote_session(cli, monkeypatch): import webbrowser import hermes_cli.auth as auth + import hermes_cli.auth_device_flow as auth_device_flow monkeypatch.setattr(auth, "_is_remote_session", lambda: True, raising=False) + monkeypatch.setattr(auth_device_flow, "_is_remote_session", lambda: True, raising=False) called = {"n": 0} monkeypatch.setattr(webbrowser, "open", lambda url: called.update(n=called["n"] + 1) or True) diff --git a/tests/hermes_cli/test_update_autostash.py b/tests/hermes_cli/test_update_autostash.py index cb9ca5cb0e..5f4c7eb179 100644 --- a/tests/hermes_cli/test_update_autostash.py +++ b/tests/hermes_cli/test_update_autostash.py @@ -764,6 +764,7 @@ def test_restore_rejects_invalid_python_and_keeps_clean_updated_tree( """A cleanly-applied stash must not be allowed to brick every agent turn.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -787,6 +788,7 @@ def test_restore_rejects_invalid_python_and_keeps_clean_updated_tree( stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ()) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ()) with pytest.raises(SystemExit) as exc_info: hermes_main._restore_stashed_changes( @@ -809,6 +811,7 @@ def test_restore_rejects_new_import_time_failure_and_preserves_stash( """A valid-Python stash must not introduce a critical import failure.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -831,6 +834,7 @@ def test_restore_rejects_new_import_time_failure_and_preserves_stash( stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) with pytest.raises(SystemExit) as exc_info: hermes_main._restore_stashed_changes( @@ -851,6 +855,7 @@ def test_restore_allows_preexisting_import_time_failure(monkeypatch, tmp_path): """A restore may proceed when it does not worsen an environment failure.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -876,6 +881,7 @@ def test_restore_allows_preexisting_import_time_failure(monkeypatch, tmp_path): stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) assert hermes_main._restore_stashed_changes( ["git"], tmp_path, stash_ref, prompt_user=False @@ -890,6 +896,7 @@ def test_restore_rejects_later_failure_masked_by_preexisting_failure( """Every critical module must be compared, not only the first failure.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -915,6 +922,7 @@ def test_restore_rejects_later_failure_masked_by_preexisting_failure( stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("first", "second")) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("first", "second")) with pytest.raises(SystemExit) as exc_info: hermes_main._restore_stashed_changes( @@ -937,6 +945,7 @@ def test_restore_rejects_system_exit_masked_by_preexisting_failure( """A terminating import must be compared instead of hiding the marker.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -962,6 +971,7 @@ def test_restore_rejects_system_exit_masked_by_preexisting_failure( stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("first", "second")) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("first", "second")) with pytest.raises(SystemExit) as exc_info: hermes_main._restore_stashed_changes( @@ -982,6 +992,7 @@ def test_restore_rejects_probe_termination(monkeypatch, tmp_path, capsys): """A stash cannot bypass import validation by terminating the probe.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -1004,6 +1015,7 @@ def test_restore_rejects_probe_termination(monkeypatch, tmp_path, capsys): stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) with pytest.raises(SystemExit) as exc_info: hermes_main._restore_stashed_changes( @@ -1025,8 +1037,10 @@ def test_restore_stays_parked_when_untracked_baseline_is_unknown( ): """Unknown cleanup scope must not turn into a destructive empty baseline.""" from hermes_cli import update_cmd + import hermes_cli.update_cmd_stash as update_cmd_stash monkeypatch.setattr(update_cmd, "_git_untracked_paths", lambda *_args: None) + monkeypatch.setattr(update_cmd_stash, "_git_untracked_paths", lambda *_args: None) restored = hermes_main._restore_stashed_changes( ["git"], tmp_path, "stash@{0}", prompt_user=False @@ -1043,8 +1057,10 @@ def test_reject_does_not_claim_cleanup_when_git_state_is_unknown( ): """Cleanup failures must not be reported as a restored clean tree.""" from hermes_cli import update_cmd + import hermes_cli.update_cmd_stash as update_cmd_stash monkeypatch.setattr(update_cmd, "_git_untracked_paths", lambda *_args: None) + monkeypatch.setattr(update_cmd_stash, "_git_untracked_paths", lambda *_args: None) with pytest.raises(SystemExit): update_cmd._reject_unsafe_stash_restore( @@ -1062,6 +1078,8 @@ def test_restore_rejects_unknown_restored_python_paths( """A failed post-apply path query cannot skip restored syntax validation.""" import subprocess from hermes_cli import update_cmd + import hermes_cli.update_cmd_stash as update_cmd_stash + import hermes_cli.update_cmd_deps as update_cmd_deps def git(*args, check=True): return subprocess.run( @@ -1083,7 +1101,9 @@ def test_restore_rejects_unknown_restored_python_paths( stash_ref = hermes_main._stash_local_changes_if_needed(["git"], tmp_path) assert stash_ref monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ()) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ()) monkeypatch.setattr(update_cmd, "_restored_python_paths", lambda *_args: None) + monkeypatch.setattr(update_cmd_stash, "_restored_python_paths", lambda *_args: None) with pytest.raises(SystemExit) as exc_info: hermes_main._restore_stashed_changes( diff --git a/tests/hermes_cli/test_update_cold_start_gateway_liveness.py b/tests/hermes_cli/test_update_cold_start_gateway_liveness.py index 5b779291a9..cac4ae3a0b 100644 --- a/tests/hermes_cli/test_update_cold_start_gateway_liveness.py +++ b/tests/hermes_cli/test_update_cold_start_gateway_liveness.py @@ -15,11 +15,13 @@ import pytest from hermes_cli import gateway as hermes_gateway from hermes_cli import gateway_windows from hermes_cli import main as cli_main +import hermes_cli.main_install_repair as main_install_repair from hermes_cli import update_cmd def _run_cold_start(monkeypatch, capsys, *, surviving_pids): monkeypatch.setattr(cli_main, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) # The pre-spawn re-check (``all_profiles=True``) must find nothing # running so the cold-start path proceeds and actually spawns. diff --git a/tests/hermes_cli/test_update_concurrent_quarantine.py b/tests/hermes_cli/test_update_concurrent_quarantine.py index add237e2ba..3e93f27dbc 100644 --- a/tests/hermes_cli/test_update_concurrent_quarantine.py +++ b/tests/hermes_cli/test_update_concurrent_quarantine.py @@ -278,6 +278,7 @@ def test_pause_and_resume_windows_gateway_service( afterward instead of spawning a competing detached gateway.""" import hermes_cli.gateway as gateway_mod import hermes_cli.update_cmd as update_cmd + import hermes_cli.update_cmd_windows as update_cmd_windows profile_home = tmp_path / "profiles" / "default" profile_home.mkdir(parents=True) @@ -312,12 +313,24 @@ def test_pause_and_resume_windows_gateway_service( lambda name, **_kwargs: stopped.append(name), raising=False, ) + monkeypatch.setattr( + update_cmd_windows, + "_stop_windows_gateway_service", + lambda name, **_kwargs: stopped.append(name), + raising=False, + ) monkeypatch.setattr( update_cmd, "_start_windows_gateway_service", lambda name: started.append(name), raising=False, ) + monkeypatch.setattr( + update_cmd_windows, + "_start_windows_gateway_service", + lambda name: started.append(name), + raising=False, + ) monkeypatch.setattr(cli_main, "_refresh_windows_gateway_launchers", lambda: None) monkeypatch.setattr( cli_main, @@ -350,6 +363,7 @@ def test_pause_windows_gateway_service_failure_restores_every_attempted_service( """A service that times out after accepting stop is restarted too.""" import hermes_cli.gateway as gateway_mod import hermes_cli.update_cmd as update_cmd + import hermes_cli.update_cmd_windows as update_cmd_windows services = [ SimpleNamespace(name="HermesGateway", service_pid=11, service_create_time=11.0, gateway_pid=101, gateway_create_time=101.0, descendant_identities=()), @@ -366,12 +380,19 @@ def test_pause_windows_gateway_service_failure_restores_every_attempted_service( restarted = [] monkeypatch.setattr(update_cmd, "_stop_windows_gateway_service", fake_stop) + monkeypatch.setattr(update_cmd_windows, "_stop_windows_gateway_service", fake_stop) monkeypatch.setattr( update_cmd, "_restore_windows_gateway_service", lambda name: restarted.append(name), raising=False, ) + monkeypatch.setattr( + update_cmd_windows, + "_restore_windows_gateway_service", + lambda name: restarted.append(name), + raising=False, + ) with pytest.raises(RuntimeError, match="HermesGatewayPicasso"): cli_main._pause_windows_gateways_for_update() @@ -386,6 +407,7 @@ def test_pause_windows_gateway_service_surfaces_rollback_start_failure( ): import hermes_cli.gateway as gateway_mod import hermes_cli.update_cmd as update_cmd + import hermes_cli.update_cmd_windows as update_cmd_windows services = [ SimpleNamespace(name="HermesGateway", service_pid=11, service_create_time=11.0, gateway_pid=101, gateway_create_time=101.0, descendant_identities=()), @@ -405,12 +427,19 @@ def test_pause_windows_gateway_service_surfaces_rollback_start_failure( raise RuntimeError("simulated rollback start failure") monkeypatch.setattr(update_cmd, "_stop_windows_gateway_service", fake_stop) + monkeypatch.setattr(update_cmd_windows, "_stop_windows_gateway_service", fake_stop) monkeypatch.setattr( update_cmd, "_restore_windows_gateway_service", fake_start, raising=False, ) + monkeypatch.setattr( + update_cmd_windows, + "_restore_windows_gateway_service", + fake_start, + raising=False, + ) with pytest.raises(RuntimeError, match="rollback failures: HermesGateway"): cli_main._pause_windows_gateways_for_update() @@ -418,6 +447,7 @@ def test_pause_windows_gateway_service_surfaces_rollback_start_failure( def test_restore_windows_gateway_service_waits_out_stop_pending(monkeypatch): import hermes_cli.update_cmd as update_cmd + import hermes_cli.update_cmd_windows as update_cmd_windows statuses = iter(["stop_pending", "stopped"]) service = SimpleNamespace(status=lambda: next(statuses)) @@ -430,6 +460,11 @@ def test_restore_windows_gateway_service_waits_out_stop_pending(monkeypatch): "_start_windows_gateway_service", lambda name: restarted.append(name), ) + monkeypatch.setattr( + update_cmd_windows, + "_start_windows_gateway_service", + lambda name: restarted.append(name), + ) update_cmd._restore_windows_gateway_service("HermesGateway") @@ -509,6 +544,7 @@ def test_resume_windows_gateway_service_failure_stays_retryable( monkeypatch, ): import hermes_cli.update_cmd as update_cmd + import hermes_cli.update_cmd_windows as update_cmd_windows token = { "resume_needed": True, @@ -522,6 +558,11 @@ def test_resume_windows_gateway_service_failure_stays_retryable( "_start_windows_gateway_service", lambda _name: (_ for _ in ()).throw(RuntimeError("simulated start failure")), ) + monkeypatch.setattr( + update_cmd_windows, + "_start_windows_gateway_service", + lambda _name: (_ for _ in ()).throw(RuntimeError("simulated start failure")), + ) with pytest.raises(RuntimeError, match="HermesGateway"): cli_main._resume_windows_gateways_after_update(token) diff --git a/tests/hermes_cli/test_update_desktop_stale_warning.py b/tests/hermes_cli/test_update_desktop_stale_warning.py index 3596029bfe..012a40df6e 100644 --- a/tests/hermes_cli/test_update_desktop_stale_warning.py +++ b/tests/hermes_cli/test_update_desktop_stale_warning.py @@ -13,6 +13,7 @@ complete`` instead of the success line, and gateway mode writes ``1`` to import pytest from hermes_cli import update_cmd +import hermes_cli.update_cmd_maint as update_cmd_maint from hermes_cli.update_cmd import ( _print_update_summary, _rebuild_desktop_after_update, @@ -137,10 +138,16 @@ def test_summary_keeps_success_banner_when_desktop_ok(capsys, monkeypatch): monkeypatch.setattr( update_cmd, "_update_complete_message", lambda _v: "✓ Update complete! (v0.20.2)" ) + monkeypatch.setattr( + update_cmd_maint, "_update_complete_message", lambda _v: "✓ Update complete! (v0.20.2)" + ) monkeypatch.setattr(update_cmd, "_branch_head_suffix", lambda *a, **k: "") monkeypatch.setattr( update_cmd, "_post_update_sqlite_runtime_status", lambda: (True, None) ) + monkeypatch.setattr( + update_cmd_maint, "_post_update_sqlite_runtime_status", lambda: (True, None) + ) _print_update_summary( node_failures=[], desktop_build_ok=True, diff --git a/tests/hermes_cli/test_update_fleet_restart_pending.py b/tests/hermes_cli/test_update_fleet_restart_pending.py index 2a57f70788..156573a92e 100644 --- a/tests/hermes_cli/test_update_fleet_restart_pending.py +++ b/tests/hermes_cli/test_update_fleet_restart_pending.py @@ -22,7 +22,11 @@ from types import SimpleNamespace import pytest from hermes_cli import main as hermes_main +import hermes_cli.main_web_build as main_web_build +import hermes_cli.main_install_repair as main_install_repair from hermes_cli import update_cmd +import hermes_cli.update_cmd_fleet as update_cmd_fleet +import hermes_cli.update_cmd_deps as update_cmd_deps from hermes_constants import get_hermes_home @@ -77,6 +81,7 @@ def _patch_update_deps(monkeypatch, tmp_path, run_side_effect): (tmp_path / ".git").mkdir() monkeypatch.setattr(hermes_main, "_resolve_update_branch", lambda args: "main") monkeypatch.setattr(hermes_main, "_is_windows", lambda: False) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: False) monkeypatch.setattr( hermes_main, "_get_origin_url", @@ -90,6 +95,9 @@ def _patch_update_deps(monkeypatch, tmp_path, run_side_effect): monkeypatch.setattr( hermes_main, "_record_bytecode_fingerprint", lambda *a, **k: None ) + monkeypatch.setattr( + main_web_build, "_record_bytecode_fingerprint", lambda *a, **k: None + ) monkeypatch.setattr(hermes_main, "_run_pre_update_backup", lambda *a, **k: None) monkeypatch.setattr( hermes_main, "_pause_windows_gateways_for_update", lambda: None @@ -99,16 +107,19 @@ def _patch_update_deps(monkeypatch, tmp_path, run_side_effect): ) monkeypatch.setattr(hermes_main, "_write_update_incomplete_marker", lambda: None) monkeypatch.setattr(hermes_main, "_clear_update_incomplete_marker", lambda: None) + monkeypatch.setattr(main_install_repair, "_clear_update_incomplete_marker", lambda: None) monkeypatch.setattr(update_cmd, "_finish_dashboard_update_cleanup", lambda *a, **k: None ) monkeypatch.setattr( update_cmd, "_finish_dashboard_update_cleanup", lambda *a, **k: None ) monkeypatch.setattr(hermes_main, "_build_web_ui", lambda *a, **k: None) + monkeypatch.setattr(main_web_build, "_build_web_ui", lambda *a, **k: None) monkeypatch.setattr( update_cmd, "_venv_core_imports_healthy", lambda: (True, "") ) monkeypatch.setattr(update_cmd, "_update_node_dependencies", lambda: []) + monkeypatch.setattr(update_cmd_deps, "_update_node_dependencies", lambda: []) monkeypatch.setattr(update_cmd, "_purge_stale_hermes_modules", lambda: None) monkeypatch.setattr(hermes_main, "_purge_stale_hermes_modules", lambda: None) @@ -168,6 +179,7 @@ def test_pending_needed_when_unfinished_receipt_runtime_sha_skews(monkeypatch): disk_sha = "e" * 40 old_sha = "7" * 40 monkeypatch.setattr(update_cmd, "_current_checkout_sha", lambda: disk_sha) + monkeypatch.setattr(update_cmd_fleet, "_current_checkout_sha", lambda: disk_sha) receipt_dir = get_hermes_home() / "logs" / "update_receipts" receipt_dir.mkdir(parents=True) @@ -205,6 +217,7 @@ def test_successful_receipt_with_pre_update_plan_shas_does_not_retrigger( disk_sha = "n" * 40 old_sha = "o" * 40 monkeypatch.setattr(update_cmd, "_current_checkout_sha", lambda: disk_sha) + monkeypatch.setattr(update_cmd_fleet, "_current_checkout_sha", lambda: disk_sha) receipt_dir = get_hermes_home() / "logs" / "update_receipts" receipt_dir.mkdir(parents=True) @@ -244,6 +257,7 @@ def test_successful_receipt_with_pre_update_plan_shas_does_not_retrigger( def test_stale_fleet_matrix_on_latest_receipt_is_pending(monkeypatch): disk_sha = "n" * 40 monkeypatch.setattr(update_cmd, "_current_checkout_sha", lambda: disk_sha) + monkeypatch.setattr(update_cmd_fleet, "_current_checkout_sha", lambda: disk_sha) receipt_dir = get_hermes_home() / "logs" / "update_receipts" receipt_dir.mkdir(parents=True) @@ -430,6 +444,7 @@ def test_already_up_to_date_runs_pending_restart_when_marker_present( return True monkeypatch.setattr(update_cmd, "_run_pending_fleet_restart", _restart) + monkeypatch.setattr(update_cmd_fleet, "_run_pending_fleet_restart", _restart) hermes_main.cmd_update(args) @@ -447,6 +462,7 @@ def test_already_up_to_date_runs_pending_restart_when_receipt_skewed( disk_sha = "e" * 40 monkeypatch.setattr(update_cmd, "_current_checkout_sha", lambda: disk_sha) + monkeypatch.setattr(update_cmd_fleet, "_current_checkout_sha", lambda: disk_sha) receipt_dir = get_hermes_home() / "logs" / "update_receipts" receipt_dir.mkdir(parents=True) (receipt_dir / "latest.json").write_text( @@ -477,6 +493,11 @@ def test_already_up_to_date_runs_pending_restart_when_receipt_skewed( "_run_pending_fleet_restart", lambda: seen.__setitem__("ran", True) or True, ) + monkeypatch.setattr( + update_cmd_fleet, + "_run_pending_fleet_restart", + lambda: seen.__setitem__("ran", True) or True, + ) hermes_main.cmd_update(args) @@ -497,6 +518,11 @@ def test_already_up_to_date_skips_restart_when_nothing_pending( "_run_pending_fleet_restart", lambda: seen.__setitem__("ran", True) or True, ) + monkeypatch.setattr( + update_cmd_fleet, + "_run_pending_fleet_restart", + lambda: seen.__setitem__("ran", True) or True, + ) hermes_main.cmd_update(args) diff --git a/tests/hermes_cli/test_update_head_moved_gate.py b/tests/hermes_cli/test_update_head_moved_gate.py index d72a8476a0..0e3faafe7b 100644 --- a/tests/hermes_cli/test_update_head_moved_gate.py +++ b/tests/hermes_cli/test_update_head_moved_gate.py @@ -14,6 +14,8 @@ from types import SimpleNamespace import pytest from hermes_cli import main as hermes_main +import hermes_cli.main_web_build as main_web_build +import hermes_cli.main_install_repair as main_install_repair from hermes_cli import update_cmd @@ -80,6 +82,7 @@ def _patch_update_deps(monkeypatch, tmp_path, run_side_effect): hermes_main, "_resolve_update_branch", lambda args: "main" ) monkeypatch.setattr(hermes_main, "_is_windows", lambda: False) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: False) monkeypatch.setattr( hermes_main, "_get_origin_url", lambda *a, **k: "https://github.com/NousResearch/hermes-agent.git", @@ -92,6 +95,9 @@ def _patch_update_deps(monkeypatch, tmp_path, run_side_effect): monkeypatch.setattr( hermes_main, "_record_bytecode_fingerprint", lambda *a, **k: None ) + monkeypatch.setattr( + main_web_build, "_record_bytecode_fingerprint", lambda *a, **k: None + ) monkeypatch.setattr( hermes_main, "_run_pre_update_backup", lambda *a, **k: None ) @@ -104,6 +110,7 @@ def _patch_update_deps(monkeypatch, tmp_path, run_side_effect): # Short-circuit the long tail: dependency install + desktop build. monkeypatch.setattr(hermes_main, "_write_update_incomplete_marker", lambda: None) monkeypatch.setattr(hermes_main, "_clear_update_incomplete_marker", lambda: None) + monkeypatch.setattr(main_install_repair, "_clear_update_incomplete_marker", lambda: None) # Gateway restart path (called after a successful update). monkeypatch.setattr(update_cmd, "_finish_dashboard_update_cleanup", lambda *a, **k: None) # Keep the (now surfaced — #78574) gateway auto-restart phase away from diff --git a/tests/hermes_cli/test_update_import_guard.py b/tests/hermes_cli/test_update_import_guard.py index 097b5a2e0d..5318892be6 100644 --- a/tests/hermes_cli/test_update_import_guard.py +++ b/tests/hermes_cli/test_update_import_guard.py @@ -22,6 +22,7 @@ import pytest from hermes_cli import main as hermes_main from hermes_cli import update_cmd +import hermes_cli.update_cmd_deps as update_cmd_deps from hermes_constants import partial_update_hint @@ -57,6 +58,7 @@ def test_syntax_guard_passes_but_import_guard_catches_skew(monkeypatch, tmp_path # The import guard catches it. monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import(tmp_path) assert ok is False assert module == "consumer" @@ -66,6 +68,7 @@ def test_syntax_guard_passes_but_import_guard_catches_skew(monkeypatch, tmp_path def test_import_guard_passes_on_consistent_tree(monkeypatch, tmp_path): _write_skewed_tree(tmp_path, skewed=False) monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) assert update_cmd._validate_critical_modules_import(tmp_path) == (True, None, None) @@ -77,6 +80,7 @@ def test_import_guard_ignores_non_import_errors(monkeypatch, tmp_path): "raise RuntimeError('no API key configured')\n" ) monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, _, _ = update_cmd._validate_critical_modules_import(tmp_path) assert ok is True @@ -86,6 +90,7 @@ def test_import_guard_can_report_non_import_errors(monkeypatch, tmp_path): """Stash restore can compare runtime failures before and after apply.""" (tmp_path / "consumer.py").write_text("raise RuntimeError('broken config')\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import( tmp_path, report_runtime_errors=True @@ -102,6 +107,7 @@ def test_import_guard_can_report_missing_third_party_dependency( """Stash comparison must see newly introduced missing dependencies.""" (tmp_path / "consumer.py").write_text("import totally_not_installed_pkg\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import( tmp_path, report_runtime_errors=True @@ -115,6 +121,7 @@ def test_import_guard_can_report_missing_third_party_dependency( def test_import_failure_comparison_preserves_exception_type(monkeypatch, tmp_path): source = tmp_path / "consumer.py" monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) source.write_text("raise RuntimeError('stopped')\n") runtime_failure = update_cmd._critical_module_import_failures( tmp_path, report_runtime_errors=True @@ -134,6 +141,7 @@ def test_import_guard_reports_probe_termination_when_comparing_states( """A terminating import is unsafe when validating a restored stash.""" (tmp_path / "consumer.py").write_text("import os\nos._exit(7)\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import( tmp_path, report_runtime_errors=True @@ -148,6 +156,7 @@ def test_import_guard_reports_probe_termination_by_default(monkeypatch, tmp_path """A missing health marker must not classify a terminated probe as healthy.""" (tmp_path / "consumer.py").write_text("import os\nos._exit(9)\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import(tmp_path) @@ -160,6 +169,7 @@ def test_import_guard_reports_system_exit_by_default(monkeypatch, tmp_path): """Catchable terminating imports must not complete with a healthy marker.""" (tmp_path / "consumer.py").write_text("raise SystemExit('stopped')\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import(tmp_path) @@ -177,6 +187,7 @@ def test_import_guard_does_not_accept_forged_static_marker(monkeypatch, tmp_path "os._exit(7)\n" ) monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import(tmp_path) @@ -311,6 +322,7 @@ def test_import_guard_ignores_missing_third_party_dependency(monkeypatch, tmp_pa """ (tmp_path / "consumer.py").write_text("import totally_not_installed_pkg\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) assert update_cmd._validate_critical_modules_import(tmp_path) == (True, None, None) @@ -319,6 +331,7 @@ def test_import_guard_flags_missing_first_party_module(monkeypatch, tmp_path): """A missing *first-party* module IS skew — the update dropped a file.""" (tmp_path / "consumer.py").write_text("import tools.nonexistent_module\n") monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + monkeypatch.setattr(update_cmd_deps, "_UPDATE_CRITICAL_MODULES", ("consumer",)) ok, module, error = update_cmd._validate_critical_modules_import(tmp_path) assert ok is False diff --git a/tests/hermes_cli/test_update_interrupted_recovery.py b/tests/hermes_cli/test_update_interrupted_recovery.py index 15c35f4a16..ea00951533 100644 --- a/tests/hermes_cli/test_update_interrupted_recovery.py +++ b/tests/hermes_cli/test_update_interrupted_recovery.py @@ -10,6 +10,7 @@ from __future__ import annotations from pathlib import Path import hermes_cli.main as m +import hermes_cli.main_install_repair as hermes_cli_main_install_repair from hermes_cli import main_install_repair from hermes_cli import update_cmd @@ -36,12 +37,14 @@ def test_marker_round_trip(tmp_path, monkeypatch): def _stub_install_env(monkeypatch, m, seen): """Common stubs so recovery's install path is inert and observable.""" + import hermes_cli.main_install_repair as hermes_cli_main_install_repair class R: returncode = 0 monkeypatch.setattr(m.subprocess, "run", lambda *a, **k: R()) monkeypatch.setattr(m, "_is_termux_env", lambda *a, **k: False) + monkeypatch.setattr(hermes_cli_main_install_repair, "_is_termux_env", lambda *a, **k: False) monkeypatch.setattr("hermes_cli.managed_uv.ensure_uv", lambda: None) # The install executor moved to hermes_cli._install_repair (shared between # the pre-import early pass and this late recovery path) — stub WHERE it @@ -70,7 +73,9 @@ def test_recovery_self_lock_does_not_clear_core_marker_via_import_probes( shim.write_text("") monkeypatch.setattr(m, "_is_windows", lambda: True) + monkeypatch.setattr(hermes_cli_main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(m, "_venv_scripts_dir", lambda: scripts_dir) + monkeypatch.setattr(hermes_cli_main_install_repair, "_venv_scripts_dir", lambda: scripts_dir) monkeypatch.setattr(main_install_repair, "_hermes_exe_shims", lambda d: [shim]) monkeypatch.setattr(main_install_repair, "_default_venv_install_target", lambda: (["uv", "pip"], {"VIRTUAL_ENV": str(tmp_path / "venv")}), @@ -78,6 +83,9 @@ def test_recovery_self_lock_does_not_clear_core_marker_via_import_probes( monkeypatch.setattr( m, "_repair_venv_via_import_probes", lambda *a, **k: "healthy" ) + monkeypatch.setattr( + hermes_cli_main_install_repair, "_repair_venv_via_import_probes", lambda *a, **k: "healthy" + ) class FakeProc: def __init__(self, exe_path): diff --git a/tests/hermes_cli/test_update_parked_branch_guard.py b/tests/hermes_cli/test_update_parked_branch_guard.py index 3d293c28fe..84b2f2e817 100644 --- a/tests/hermes_cli/test_update_parked_branch_guard.py +++ b/tests/hermes_cli/test_update_parked_branch_guard.py @@ -26,6 +26,8 @@ from types import SimpleNamespace import pytest from hermes_cli import main as hermes_main +import hermes_cli.main_web_build as main_web_build +import hermes_cli.main_install_repair as main_install_repair from hermes_cli import update_cmd @@ -251,6 +253,7 @@ def _patch_update_flow(monkeypatch, repo, run_real_git=True): monkeypatch.setattr(hermes_main, "PROJECT_ROOT", repo) monkeypatch.setattr(hermes_main, "_resolve_update_branch", lambda args: "main") monkeypatch.setattr(hermes_main, "_is_windows", lambda: False) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: False) monkeypatch.setattr( hermes_main, "_get_origin_url", lambda *a, **k: "https://github.com/NousResearch/hermes-agent.git", @@ -261,6 +264,7 @@ def _patch_update_flow(monkeypatch, repo, run_real_git=True): monkeypatch.setattr(update_cmd, "_normalize_managed_eol", lambda *a, **k: None) monkeypatch.setattr(hermes_main, "_clear_bytecode_cache", lambda *a, **k: 0) monkeypatch.setattr(hermes_main, "_record_bytecode_fingerprint", lambda *a, **k: None) + monkeypatch.setattr(main_web_build, "_record_bytecode_fingerprint", lambda *a, **k: None) monkeypatch.setattr(hermes_main, "_run_pre_update_backup", lambda *a, **k: None) monkeypatch.setattr(hermes_main, "_pause_windows_gateways_for_update", lambda: None) monkeypatch.setattr( diff --git a/tests/hermes_cli/test_update_sqlite_remediation.py b/tests/hermes_cli/test_update_sqlite_remediation.py index e2a280759a..e0dae08044 100644 --- a/tests/hermes_cli/test_update_sqlite_remediation.py +++ b/tests/hermes_cli/test_update_sqlite_remediation.py @@ -4,6 +4,8 @@ from pathlib import Path from types import SimpleNamespace from hermes_cli import update_cmd +import hermes_cli.update_cmd_maint as update_cmd_maint +import hermes_cli.update_cmd_deps as update_cmd_deps def test_runtime_status_probes_running_venv_outside_checkout(tmp_path, monkeypatch): @@ -31,11 +33,22 @@ def test_summary_withholds_success_when_sqlite_remediation_failed(capsys, monkey lambda: (False, SimpleNamespace(sqlite_version_string="3.46.1")), raising=False, ) + monkeypatch.setattr( + update_cmd_maint, + "_post_update_sqlite_runtime_status", + lambda: (False, SimpleNamespace(sqlite_version_string="3.46.1")), + raising=False, + ) monkeypatch.setattr( update_cmd, "_update_complete_message", lambda _version: "✓ Update complete! (v0.20.5)", ) + monkeypatch.setattr( + update_cmd_maint, + "_update_complete_message", + lambda _version: "✓ Update complete! (v0.20.5)", + ) complete = update_cmd._print_update_summary( node_failures=[], @@ -58,6 +71,11 @@ def test_current_checkout_completion_is_verified_before_success(capsys, monkeypa "_post_update_sqlite_runtime_status", lambda: (False, SimpleNamespace(sqlite_version_string="3.46.1")), ) + monkeypatch.setattr( + update_cmd_maint, + "_post_update_sqlite_runtime_status", + lambda: (False, SimpleNamespace(sqlite_version_string="3.46.1")), + ) complete = update_cmd._print_verified_update_completion("✓ Already up to date!") @@ -69,12 +87,18 @@ def test_current_checkout_completion_is_verified_before_success(capsys, monkeypa def test_current_checkout_repair_returns_verified_completion_result(monkeypatch): monkeypatch.setattr(update_cmd, "_update_node_dependencies", lambda: []) + monkeypatch.setattr(update_cmd_deps, "_update_node_dependencies", lambda: []) monkeypatch.setattr(update_cmd._m(), "_build_web_ui", lambda _path: None) monkeypatch.setattr( update_cmd, "_rebuild_desktop_after_update", lambda _dir, **_kwargs: True, ) + monkeypatch.setattr( + update_cmd_deps, + "_rebuild_desktop_after_update", + lambda _dir, **_kwargs: True, + ) complete = update_cmd._repair_node_deps_on_current_checkout( lambda _message: False diff --git a/tests/hermes_cli/test_update_venv_ownership_preflight.py b/tests/hermes_cli/test_update_venv_ownership_preflight.py index 56d9c67fcd..9a7820c2e4 100644 --- a/tests/hermes_cli/test_update_venv_ownership_preflight.py +++ b/tests/hermes_cli/test_update_venv_ownership_preflight.py @@ -16,6 +16,7 @@ import os import pytest from hermes_cli import update_cmd +import hermes_cli.update_cmd_deps as update_cmd_deps def _make_fake_venv(tmp_path): @@ -59,6 +60,7 @@ def test_foreign_owned_dist_info_child_detected(tmp_path, monkeypatch): return real_uid(path) monkeypatch.setattr(update_cmd, "_path_uid", fake_uid) + monkeypatch.setattr(update_cmd_deps, "_path_uid", fake_uid) foreign = update_cmd._venv_foreign_owned_paths(venv) assert foreign == [(installer, 0)] @@ -72,6 +74,11 @@ def test_foreign_owned_refuses_with_chown_hint(tmp_path, monkeypatch, capsys): "_path_uid", lambda p: 0 if str(p) == hermes_bin else real_uid(p), ) + monkeypatch.setattr( + update_cmd_deps, + "_path_uid", + lambda p: 0 if str(p) == hermes_bin else real_uid(p), + ) with pytest.raises(SystemExit) as exc: update_cmd._refuse_update_if_venv_foreign_owned(tmp_path) assert exc.value.code == 1 @@ -92,6 +99,11 @@ def test_limit_caps_reported_paths(tmp_path, monkeypatch): "_path_uid", lambda p: 0 if str(p).startswith(str(bin_dir) + os.sep) else 12345, ) + monkeypatch.setattr( + update_cmd_deps, + "_path_uid", + lambda p: 0 if str(p).startswith(str(bin_dir) + os.sep) else 12345, + ) monkeypatch.setattr(update_cmd.os, "geteuid", lambda: 12345, raising=False) foreign = update_cmd._venv_foreign_owned_paths(venv, limit=3) assert len(foreign) == 3 @@ -116,6 +128,7 @@ def test_running_as_root_returns_empty(tmp_path, monkeypatch): monkeypatch.setattr(update_cmd.os, "geteuid", lambda: 0, raising=False) # Even with foreign uids everywhere, root skips the gate. monkeypatch.setattr(update_cmd, "_path_uid", lambda p: 4242) + monkeypatch.setattr(update_cmd_deps, "_path_uid", lambda p: 4242) assert update_cmd._venv_foreign_owned_paths(venv) == [] diff --git a/tests/hermes_cli/test_update_zip_fallback_guards.py b/tests/hermes_cli/test_update_zip_fallback_guards.py index 61484de2cd..e68cf66a6f 100644 --- a/tests/hermes_cli/test_update_zip_fallback_guards.py +++ b/tests/hermes_cli/test_update_zip_fallback_guards.py @@ -17,6 +17,7 @@ from unittest.mock import patch import pytest from hermes_cli import main as hermes_main +import hermes_cli.main_install_repair as main_install_repair from hermes_cli import update_cmd @@ -74,18 +75,21 @@ def test_unknown_command_gets_generic_stage(): def test_windows_dep_failure_does_not_zip_fallback(monkeypatch): monkeypatch.setattr(hermes_main, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) exc = _cpe([r"C:\venv\Scripts\uv.exe", "pip", "install", "-e", "."]) assert update_cmd._should_zip_fallback_on_update_error(exc) is False def test_windows_git_failure_still_zips(monkeypatch): monkeypatch.setattr(hermes_main, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) exc = _cpe(["git", "pull"], returncode=1) assert update_cmd._should_zip_fallback_on_update_error(exc) is True def test_posix_git_failure_does_not_zip(monkeypatch): monkeypatch.setattr(hermes_main, "_is_windows", lambda: False) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: False) exc = _cpe(["git", "pull"], returncode=1) assert update_cmd._should_zip_fallback_on_update_error(exc) is False diff --git a/tests/hermes_cli/test_web_routers_tools_install_on_enable.py b/tests/hermes_cli/test_web_routers_tools_install_on_enable.py index 3c450994c8..60d415efd2 100644 --- a/tests/hermes_cli/test_web_routers_tools_install_on_enable.py +++ b/tests/hermes_cli/test_web_routers_tools_install_on_enable.py @@ -50,12 +50,13 @@ class TestToggleToolsetInstallOnEnable: self, monkeypatch ): import hermes_cli.tools_config as tools_config + import hermes_cli.tools_config_cua as tools_config_cua import hermes_cli.tools_config_post_setup as tools_config_post_setup calls = self._spawn_recorder(monkeypatch) # Binary missing → the cua_driver predicate reports unsatisfied. monkeypatch.setattr( - tools_config, "_resolved_cua_driver_cmd", lambda: None + tools_config_cua, "_resolved_cua_driver_cmd", lambda: None ) monkeypatch.setattr( tools_config_post_setup, "_cua_driver_install_ready", lambda: False @@ -77,11 +78,12 @@ class TestToggleToolsetInstallOnEnable: self, monkeypatch ): import hermes_cli.tools_config as tools_config + import hermes_cli.tools_config_cua as tools_config_cua import hermes_cli.tools_config_post_setup as tools_config_post_setup calls = self._spawn_recorder(monkeypatch) monkeypatch.setattr( - tools_config, "_resolved_cua_driver_cmd", lambda: "/usr/bin/cua-driver" + tools_config_cua, "_resolved_cua_driver_cmd", lambda: "/usr/bin/cua-driver" ) monkeypatch.setattr( tools_config_post_setup, "_cua_driver_install_ready", lambda: True @@ -96,11 +98,12 @@ class TestToggleToolsetInstallOnEnable: def test_disable_never_spawns_install(self, monkeypatch): import hermes_cli.tools_config as tools_config + import hermes_cli.tools_config_cua as tools_config_cua import hermes_cli.tools_config_post_setup as tools_config_post_setup calls = self._spawn_recorder(monkeypatch) monkeypatch.setattr( - tools_config, "_resolved_cua_driver_cmd", lambda: None + tools_config_cua, "_resolved_cua_driver_cmd", lambda: None ) monkeypatch.setattr( tools_config_post_setup, "_cua_driver_install_ready", lambda: False @@ -115,11 +118,12 @@ class TestToggleToolsetInstallOnEnable: def test_spawn_failure_does_not_fail_the_toggle(self, monkeypatch): import hermes_cli.tools_config as tools_config + import hermes_cli.tools_config_cua as tools_config_cua import hermes_cli.tools_config_post_setup as tools_config_post_setup import hermes_cli.web_server as web_server monkeypatch.setattr( - tools_config, "_resolved_cua_driver_cmd", lambda: None + tools_config_cua, "_resolved_cua_driver_cmd", lambda: None ) monkeypatch.setattr( tools_config_post_setup, "_cua_driver_install_ready", lambda: False diff --git a/tests/hermes_cli/test_windows_gateway_cold_start_desktop_lifecycle.py b/tests/hermes_cli/test_windows_gateway_cold_start_desktop_lifecycle.py index bc359d5c19..e1454f0fcc 100644 --- a/tests/hermes_cli/test_windows_gateway_cold_start_desktop_lifecycle.py +++ b/tests/hermes_cli/test_windows_gateway_cold_start_desktop_lifecycle.py @@ -14,8 +14,10 @@ from __future__ import annotations from hermes_cli import gateway as hermes_gateway from hermes_cli import gateway_windows from hermes_cli import main as cli_main +import hermes_cli.main_install_repair as main_install_repair from hermes_cli import process_identity from hermes_cli import update_cmd +import hermes_cli.update_cmd_windows as update_cmd_windows def _live_serve_ledger_entry() -> dict: @@ -86,24 +88,28 @@ def test_orphaned_control_plane_does_not_own_lifecycle(monkeypatch): def test_pause_skips_cold_start_plan_when_desktop_owns_lifecycle(monkeypatch): monkeypatch.setattr(cli_main, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hermes_gateway, "find_gateway_pids", lambda **_k: []) monkeypatch.setattr( hermes_gateway, "find_windows_gateway_services", lambda **_k: [] ) monkeypatch.setattr(gateway_windows, "is_installed", lambda: True) monkeypatch.setattr(update_cmd, "_desktop_owns_gateway_lifecycle", lambda: True) + monkeypatch.setattr(update_cmd_windows, "_desktop_owns_gateway_lifecycle", lambda: True) assert update_cmd._pause_windows_gateways_for_update() is None def test_pause_still_cold_starts_when_autostart_and_no_desktop_owner(monkeypatch): monkeypatch.setattr(cli_main, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hermes_gateway, "find_gateway_pids", lambda **_k: []) monkeypatch.setattr( hermes_gateway, "find_windows_gateway_services", lambda **_k: [] ) monkeypatch.setattr(gateway_windows, "is_installed", lambda: True) monkeypatch.setattr(update_cmd, "_desktop_owns_gateway_lifecycle", lambda: False) + monkeypatch.setattr(update_cmd_windows, "_desktop_owns_gateway_lifecycle", lambda: False) token = update_cmd._pause_windows_gateways_for_update() @@ -119,8 +125,10 @@ def test_pause_still_cold_starts_when_autostart_and_no_desktop_owner(monkeypatch def test_cold_start_aborts_when_desktop_owns_lifecycle(monkeypatch): spawned = [] monkeypatch.setattr(cli_main, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hermes_gateway, "find_gateway_pids", lambda **_k: []) monkeypatch.setattr(update_cmd, "_desktop_owns_gateway_lifecycle", lambda: True) + monkeypatch.setattr(update_cmd_windows, "_desktop_owns_gateway_lifecycle", lambda: True) monkeypatch.setattr( gateway_windows, "_spawn_detached", lambda: spawned.append(1) or 4242 ) diff --git a/tests/hermes_cli/test_windows_gateway_job_teardown_48820.py b/tests/hermes_cli/test_windows_gateway_job_teardown_48820.py index 285f0b1b6c..78d9dd5187 100644 --- a/tests/hermes_cli/test_windows_gateway_job_teardown_48820.py +++ b/tests/hermes_cli/test_windows_gateway_job_teardown_48820.py @@ -27,6 +27,7 @@ import pytest import hermes_cli.gateway as gateway import hermes_cli.gateway_windows as gateway_windows import hermes_cli.main as hm +import hermes_cli.main_install_repair as main_install_repair from hermes_cli._subprocess_compat import _WINDOWS_GATEWAY_BREAKAWAY_ENV from hermes_cli.update_cmd import _resume_windows_gateways_after_update @@ -119,6 +120,7 @@ class TestResumeLivenessGate: @pytest.fixture(autouse=True) def _windows(self, monkeypatch): monkeypatch.setattr(hm, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hm, "_refresh_windows_gateway_launchers", lambda: None) monkeypatch.setattr( gateway, "launch_detached_profile_gateway_restart", lambda *_a: True diff --git a/tests/hermes_cli/test_windows_update_restart_reconciliation.py b/tests/hermes_cli/test_windows_update_restart_reconciliation.py index 65bb3b0c82..33b8e6bc7e 100644 --- a/tests/hermes_cli/test_windows_update_restart_reconciliation.py +++ b/tests/hermes_cli/test_windows_update_restart_reconciliation.py @@ -26,6 +26,7 @@ import pytest import hermes_cli.gateway as gateway import hermes_cli.gateway_windows as gateway_windows import hermes_cli.main as hm +import hermes_cli.main_install_repair as main_install_repair from hermes_cli.update_cmd import _resume_windows_gateways_after_update from hermes_cli.update_inventory import ( RuntimeRecord, @@ -61,6 +62,7 @@ def _stub_post_relaunch_liveness(monkeypatch): def test_resume_records_successfully_relaunched_profiles_on_the_token(monkeypatch): monkeypatch.setattr(hm, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hm, "_refresh_windows_gateway_launchers", lambda: None) monkeypatch.setattr( gateway, "launch_detached_profile_gateway_restart", lambda *_a: True @@ -81,6 +83,7 @@ def test_resume_omits_profiles_whose_relaunch_failed(monkeypatch): 'relaunched' — it needs to keep surfacing as unaccounted so the user is told to restart it manually (Windows has no watcher to recover it).""" monkeypatch.setattr(hm, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hm, "_refresh_windows_gateway_launchers", lambda: None) def _relaunch(profile, _old_pid): @@ -110,6 +113,7 @@ def test_merged_windows_relaunch_resolves_as_restarted_not_unaccounted(monkeypat match_runtime_outcomes, must turn a Windows gateway's plan row from 'unaccounted' (loud warning + exit 1) into 'restarted' (clean).""" monkeypatch.setattr(hm, "_is_windows", lambda: True) + monkeypatch.setattr(main_install_repair, "_is_windows", lambda: True) monkeypatch.setattr(hm, "_refresh_windows_gateway_launchers", lambda: None) monkeypatch.setattr( gateway, "launch_detached_profile_gateway_restart", lambda *_a: True diff --git a/tests/test_resource_limits.py b/tests/test_resource_limits.py index 51a9e8c553..22018f30a7 100644 --- a/tests/test_resource_limits.py +++ b/tests/test_resource_limits.py @@ -205,6 +205,7 @@ async def test_gateway_startup_applies_limit_before_gateway_initialization(monke def test_serve_startup_applies_limit_before_web_server(monkeypatch): from hermes_cli import main as cli_main + import hermes_cli.main_web_build as main_web_build import hermes_cli.plugins import hermes_cli.web_server @@ -223,6 +224,7 @@ def test_serve_startup_applies_limit_before_web_server(monkeypatch): ) monkeypatch.setattr(cli_main, "_sync_bundled_skills_quietly", lambda: None) monkeypatch.setattr(cli_main, "_build_web_ui", lambda *args, **kwargs: True) + monkeypatch.setattr(main_web_build, "_build_web_ui", lambda *args, **kwargs: True) monkeypatch.setattr(cli_main, "_maybe_setup_dashboard_auth_interactively", lambda args: None) monkeypatch.setattr(hermes_cli.plugins, "discover_plugins", lambda: None) monkeypatch.setattr( @@ -320,6 +322,7 @@ def test_named_profile_reroute_defers_limit_to_final_process(monkeypatch, tmp_pa def test_dashboard_lifecycle_flags_skip_limit_adjustment(monkeypatch, lifecycle_flag): """Informational/stop-only commands must not mutate process limits.""" from hermes_cli import main as cli_main + import hermes_cli.main_dashboard as hermes_cli_main_dashboard calls: list[str] = [] monkeypatch.setattr( @@ -329,6 +332,7 @@ def test_dashboard_lifecycle_flags_skip_limit_adjustment(monkeypatch, lifecycle_ ) monkeypatch.setattr(dashboard_procs, "_scan_dashboard_processes", lambda: []) monkeypatch.setattr(cli_main, "_find_stale_dashboard_pids", lambda: []) + monkeypatch.setattr(hermes_cli_main_dashboard, "_find_stale_dashboard_pids", lambda: []) args = SimpleNamespace( status=lifecycle_flag == "status", diff --git a/tests/tools/test_approval_deny_rules.py b/tests/tools/test_approval_deny_rules.py index e809c7912c..90abde3d74 100644 --- a/tests/tools/test_approval_deny_rules.py +++ b/tests/tools/test_approval_deny_rules.py @@ -10,6 +10,7 @@ import os import pytest from tools import approval as mod +import tools.approval_floors as approval_floors from tools import approval_context @@ -109,6 +110,8 @@ class TestDenyOrdering: deny_config(["git push --force*"]) monkeypatch.setattr( mod, "_command_matches_permanent_allowlist", lambda c: True) + monkeypatch.setattr( + approval_floors, "_command_matches_permanent_allowlist", lambda c: True) result = mod.check_dangerous_command("git push --force origin main", "local") assert result["approved"] is False diff --git a/tests/tools/test_delegate_kanban_isolation.py b/tests/tools/test_delegate_kanban_isolation.py index ea08038645..27d45ddcc0 100644 --- a/tests/tools/test_delegate_kanban_isolation.py +++ b/tests/tools/test_delegate_kanban_isolation.py @@ -100,9 +100,11 @@ def test_build_child_agent_strips_kanban_toolset_even_when_parent_is_worker(monk import run_agent from tools import delegate_tool + import tools.delegate_tool_config as delegate_tool_config monkeypatch.setattr(run_agent, "AIAgent", FakeAgent) monkeypatch.setattr(delegate_tool, "_load_config", lambda: {}) + monkeypatch.setattr(delegate_tool_config, "_load_config", lambda: {}) class Parent: enabled_toolsets = ["terminal", "kanban"] diff --git a/tests/tools/test_denial_circuit_breaker.py b/tests/tools/test_denial_circuit_breaker.py index 63feb7b53e..e5b2627ef6 100644 --- a/tests/tools/test_denial_circuit_breaker.py +++ b/tests/tools/test_denial_circuit_breaker.py @@ -16,6 +16,8 @@ from __future__ import annotations import pytest from tools import approval as A +import tools.approval_prompt as approval_prompt +import tools.approval_detection as approval_detection from tools import approval_context from tools import approval_smart @@ -42,6 +44,10 @@ def breaker_session(monkeypatch): A, "detect_dangerous_command", lambda command: (True, "breaker-test-danger", f"risk:{command}"), ) + monkeypatch.setattr( + approval_detection, "detect_dangerous_command", + lambda command: (True, "breaker-test-danger", f"risk:{command}"), + ) monkeypatch.setattr( "tools.tirith_security.check_command_security", lambda _command: {"action": "allow", "findings": [], "summary": ""}, @@ -179,6 +185,10 @@ def test_headless_smart_deny_increments_and_trips(monkeypatch): A, "detect_dangerous_command", lambda command: (True, "headless-breaker-danger", f"risk:{command}"), ) + monkeypatch.setattr( + approval_detection, "detect_dangerous_command", + lambda command: (True, "headless-breaker-danger", f"risk:{command}"), + ) monkeypatch.setattr( "tools.tirith_security.check_command_security", lambda _command: {"action": "allow", "findings": [], "summary": ""}, @@ -187,6 +197,8 @@ def test_headless_smart_deny_increments_and_trips(monkeypatch): # CLI-interactive path: the owner denies via the prompt callback. monkeypatch.setattr(A, "prompt_dangerous_approval", lambda *args, **kwargs: "deny") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", + lambda *args, **kwargs: "deny") session_key = "headless-breaker-session" token = approval_context.set_current_session_key(session_key) diff --git a/tests/tools/test_execute_code_approval_cluster.py b/tests/tools/test_execute_code_approval_cluster.py index 0b2bd15aa9..a5664b1faf 100644 --- a/tests/tools/test_execute_code_approval_cluster.py +++ b/tests/tools/test_execute_code_approval_cluster.py @@ -23,6 +23,7 @@ import threading import pytest from tools import approval as A +import tools.approval_detection as approval_detection from tools import approval_context from tools import approval_context from tools import approval_smart @@ -294,6 +295,11 @@ def test_terminal_smart_deny_owner_override_is_one_operation(gw_session, monkeyp "detect_dangerous_command", lambda command: (True, "owner-override-test-danger", f"risk:{command}"), ) + monkeypatch.setattr( + approval_detection, + "detect_dangerous_command", + lambda command: (True, "owner-override-test-danger", f"risk:{command}"), + ) monkeypatch.setattr( "tools.tirith_security.check_command_security", lambda _command: {"action": "allow", "findings": [], "summary": ""}, @@ -349,6 +355,10 @@ def test_smart_escalate_still_persists_session_choice(gw_session, monkeypatch): A, "detect_dangerous_command", lambda command: (True, key, f"risk:{command}"), ) + monkeypatch.setattr( + approval_detection, "detect_dangerous_command", + lambda command: (True, key, f"risk:{command}"), + ) monkeypatch.setattr( "tools.tirith_security.check_command_security", lambda _command: {"action": "allow", "findings": [], "summary": ""}, @@ -371,6 +381,10 @@ def test_terminal_smart_deny_pending_payload_is_one_operation(gw_session, monkey A, "detect_dangerous_command", lambda command: (True, "pending-smart-deny", f"risk:{command}"), ) + monkeypatch.setattr( + approval_detection, "detect_dangerous_command", + lambda command: (True, "pending-smart-deny", f"risk:{command}"), + ) monkeypatch.setattr( "tools.tirith_security.check_command_security", lambda _command: {"action": "allow", "findings": [], "summary": ""}, diff --git a/tests/tools/test_request_tool_approval.py b/tests/tools/test_request_tool_approval.py index 94b1292b75..51b6e539e0 100644 --- a/tests/tools/test_request_tool_approval.py +++ b/tests/tools/test_request_tool_approval.py @@ -9,6 +9,8 @@ the gateway submit_pending path, cron_mode, and fail-closed timeouts. import pytest import tools.approval as approval +import tools.approval_prompt as approval_prompt +import tools.approval_context as tools_approval_context from tools import approval_context from tools.approval import request_tool_approval @@ -20,6 +22,10 @@ def _isolate_approval_state(monkeypatch): approval, "get_current_session_key", lambda default="default": "test-session", ) + monkeypatch.setattr( + tools_approval_context, "get_current_session_key", + lambda default="default": "test-session", + ) # Empty session + permanent approval stores so nothing pre-approves. monkeypatch.setattr(approval, "is_approved", lambda sk, pk: False) # Not a yolo session (the shared gate checks this first). @@ -40,13 +46,19 @@ class TestRequestToolApproval: approval, "prompt_dangerous_approval", lambda *a, **k: pytest.fail("should not prompt when already approved"), ) + monkeypatch.setattr( + approval_prompt, "prompt_dangerous_approval", + lambda *a, **k: pytest.fail("should not prompt when already approved"), + ) res = request_tool_approval("write_file", "sensitive path", rule_key="ssh") assert res == {"approved": True, "message": None} def test_cli_approve_once(self, monkeypatch): monkeypatch.setattr(approval, "_is_interactive_cli", lambda: True) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "prompt_dangerous_approval", lambda *a, **k: "once") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", lambda *a, **k: "once") res = request_tool_approval("write_file", "writing ~/.ssh/authorized_keys") assert res["approved"] is True @@ -55,7 +67,9 @@ class TestRequestToolApproval: monkeypatch.setattr(approval, "_is_interactive_cli", lambda: True) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "prompt_dangerous_approval", lambda *a, **k: "deny") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", lambda *a, **k: "deny") events = [] monkeypatch.setattr( lifecycle, @@ -84,7 +98,9 @@ class TestRequestToolApproval: def test_cli_session_persists_session_only(self, monkeypatch): monkeypatch.setattr(approval, "_is_interactive_cli", lambda: True) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "prompt_dangerous_approval", lambda *a, **k: "session") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", lambda *a, **k: "session") calls = {"session": [], "permanent": []} monkeypatch.setattr(approval, "approve_session", lambda sk, pk: calls["session"].append(pk)) @@ -100,7 +116,9 @@ class TestRequestToolApproval: def test_cron_deny_mode_blocks(self, monkeypatch): monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: True) + monkeypatch.setattr(tools_approval_context, "_is_cron_approval_context", lambda: True) monkeypatch.setattr(approval_context, "_get_cron_approval_mode", lambda: "deny") res = request_tool_approval("terminal", "smtp send") assert res["approved"] is False @@ -109,7 +127,9 @@ class TestRequestToolApproval: def test_cron_approve_mode_allows(self, monkeypatch): monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: True) + monkeypatch.setattr(tools_approval_context, "_is_cron_approval_context", lambda: True) monkeypatch.setattr(approval_context, "_get_cron_approval_mode", lambda: "approve") res = request_tool_approval("terminal", "smtp send") assert res["approved"] is True @@ -120,7 +140,9 @@ class TestRequestToolApproval: allowlist entry (Finding 3: tool_name alone was too coarse).""" monkeypatch.setattr(approval, "_is_interactive_cli", lambda: True) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "prompt_dangerous_approval", lambda *a, **k: "deny") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", lambda *a, **k: "deny") k1 = request_tool_approval("write_file", "write to ~/.ssh")["pattern_key"] k2 = request_tool_approval("write_file", "send an email")["pattern_key"] assert k1 != k2 @@ -128,7 +150,9 @@ class TestRequestToolApproval: def test_explicit_rule_key_overrides_derivation(self, monkeypatch): monkeypatch.setattr(approval, "_is_interactive_cli", lambda: True) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "prompt_dangerous_approval", lambda *a, **k: "deny") + monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", lambda *a, **k: "deny") res = request_tool_approval("terminal", "any", rule_key="my-rule") assert res["pattern_key"] == "plugin_rule:my-rule" @@ -137,7 +161,9 @@ class TestRequestToolApproval: — a plugin-flagged action never runs ungated without a human.""" monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False) monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_gateway_approval_context", lambda: False) monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: False) + monkeypatch.setattr(tools_approval_context, "_is_cron_approval_context", lambda: False) res = request_tool_approval("terminal", "smtp send") assert res["approved"] is False assert "no interactive user or gateway" in res["message"].lower() @@ -150,5 +176,9 @@ class TestRequestToolApproval: approval, "prompt_dangerous_approval", lambda *a, **k: pytest.fail("yolo must not prompt"), ) + monkeypatch.setattr( + approval_prompt, "prompt_dangerous_approval", + lambda *a, **k: pytest.fail("yolo must not prompt"), + ) res = request_tool_approval("terminal", "curl PUT", rule_key="ext") assert res == {"approved": True, "message": None} diff --git a/tests/tools/test_skills_sync_client.py b/tests/tools/test_skills_sync_client.py index dbe647069e..449194c9fd 100644 --- a/tests/tools/test_skills_sync_client.py +++ b/tests/tools/test_skills_sync_client.py @@ -310,8 +310,11 @@ class TestDevGate: ) # patch the lazily-imported symbol used inside resolve_identity import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token, "base_url": "https://x"}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token, "base_url": "https://x"}) ident = ssc.resolve_identity() assert ident["nous_admin"] is True assert ident["owner"] == "user1" @@ -319,34 +322,45 @@ class TestDevGate: def test_gate_closed_without_claim(self, monkeypatch): token = _jwt({"sub": "user1"}) # no tool_gateway_admin import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token, "base_url": "https://x"}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token, "base_url": "https://x"}) ident = ssc.resolve_identity() assert ident["nous_admin"] is False def test_gate_closed_when_claim_false(self, monkeypatch): token = _jwt({"sub": "u", "tool_gateway_admin": False}) import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token, "base_url": "https://x"}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token, "base_url": "https://x"}) assert ssc.resolve_identity()["nous_admin"] is False def test_maybe_push_inert_when_gate_closed(self, monkeypatch): token = _jwt({"sub": "u"}) import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token}) monkeypatch.setattr(ssc, "resolve_sync_base_url", lambda: "http://x") # gate closed -> None (inert), never attempts a push assert ssc.maybe_push_skills() is None def test_maybe_pull_inert_when_not_logged_in(self, monkeypatch): import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous def _raise(**kw): raise RuntimeError("not logged in") monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", _raise) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", _raise) assert ssc.maybe_pull_skills() is None @@ -880,16 +894,22 @@ class TestOrgIdentityGate: # Personal org: NAS stamps NO org_role -> inert, not an error path. token = _jwt({"sub": "u", "org_id": "org-1"}) import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token, "base_url": "https://x"}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token, "base_url": "https://x"}) with pytest.raises(ssc.SyncInertError): ssc.resolve_org_identity() def test_org_identity_with_role(self, monkeypatch): token = _jwt({"sub": "u", "org_id": "org-9", "org_role": "MEMBER"}) import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token, "base_url": "https://x"}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token, "base_url": "https://x"}) ident = ssc.resolve_org_identity() assert ident["org_id"] == "org-9" assert ident["org_role"] == "MEMBER" @@ -1006,8 +1026,11 @@ class TestOrgEndToEnd: # Personal org: no org_role claim -> None, never raises. token = _jwt({"sub": "u", "org_id": "org-1"}) import hermes_cli.auth as auth_mod + import hermes_cli.auth_nous as auth_nous monkeypatch.setattr(auth_mod, "resolve_nous_runtime_credentials", lambda **kw: {"api_key": token}) + monkeypatch.setattr(auth_nous, "resolve_nous_runtime_credentials", + lambda **kw: {"api_key": token}) assert org.maybe_pull_org_skills() is None diff --git a/tests/tools/test_xai_http_credentials.py b/tests/tools/test_xai_http_credentials.py index 28f249b77e..88444d30ba 100644 --- a/tests/tools/test_xai_http_credentials.py +++ b/tests/tools/test_xai_http_credentials.py @@ -3,8 +3,10 @@ import pytest def _set_xai_oauth_unavailable(monkeypatch): from hermes_cli import auth + import hermes_cli.auth_xai as auth_xai monkeypatch.setattr(auth, "resolve_xai_oauth_runtime_credentials", lambda **_: {}) + monkeypatch.setattr(auth_xai, "resolve_xai_oauth_runtime_credentials", lambda **_: {}) def test_xai_credentials_fail_closed_without_profile_scope(tmp_path, monkeypatch):