fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, 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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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 <other>``) 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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == []
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 == []
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) == []
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": ""},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user