mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
feat(auth) normalise the way in which we check whether a user has free/paid access to nous portal so we can expose behaviour and error messages accordingly.
This commit is contained in:
parent
0bf9b867cf
commit
406901b27d
32 changed files with 2470 additions and 181 deletions
|
|
@ -667,6 +667,42 @@ def test_get_nous_auth_status_checks_credential_pool(tmp_path, monkeypatch):
|
|||
assert "example.com" in str(status.get("portal_base_url", ""))
|
||||
|
||||
|
||||
def test_get_nous_auth_status_pool_opaque_key_is_not_portal_login(tmp_path, monkeypatch):
|
||||
from hermes_cli.auth import get_nous_auth_status, invalidate_nous_auth_status_cache
|
||||
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
(hermes_home / "auth.json").write_text(json.dumps({
|
||||
"version": 1, "providers": {},
|
||||
}))
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
invalidate_nous_auth_status_cache()
|
||||
|
||||
from agent.credential_pool import PooledCredential, load_pool
|
||||
pool = load_pool("nous")
|
||||
entry = PooledCredential.from_dict("nous", {
|
||||
"access_token": "",
|
||||
"agent_key": "opaque-agent-key",
|
||||
"agent_key_expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"label": "manual opaque key",
|
||||
"auth_type": "api_key",
|
||||
"source": "manual",
|
||||
"base_url": "https://inference.example.com/v1",
|
||||
"inference_base_url": "https://inference.example.com/v1",
|
||||
})
|
||||
pool.add_entry(entry)
|
||||
|
||||
status = get_nous_auth_status()
|
||||
|
||||
assert status["logged_in"] is False
|
||||
assert status["inference_credential_present"] is True
|
||||
assert status["credential_source"] == "pool:manual opaque key"
|
||||
assert status.get("access_token") is None
|
||||
assert status.get("portal_base_url") is None
|
||||
assert status.get("inference_base_url") == "https://inference.example.com/v1"
|
||||
invalidate_nous_auth_status_cache()
|
||||
|
||||
|
||||
def test_get_nous_auth_status_auth_store_fallback(tmp_path, monkeypatch):
|
||||
"""get_nous_auth_status() falls back to auth store when credential
|
||||
pool is empty.
|
||||
|
|
@ -1023,12 +1059,19 @@ class TestLoginNousSkipKeepsCurrent:
|
|||
lambda *a, **kw: prompt_returns,
|
||||
)
|
||||
monkeypatch.setattr(models_mod, "get_pricing_for_provider", lambda p: {})
|
||||
monkeypatch.setattr(models_mod, "check_nous_free_tier", lambda: None)
|
||||
free_tier_calls = []
|
||||
|
||||
def _check_nous_free_tier(**kwargs):
|
||||
free_tier_calls.append(kwargs)
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(models_mod, "check_nous_free_tier", _check_nous_free_tier)
|
||||
monkeypatch.setattr(
|
||||
models_mod, "partition_nous_models_by_tier",
|
||||
lambda ids, p, free_tier=False: (ids, []),
|
||||
)
|
||||
monkeypatch.setattr(ns, "prompt_enable_tool_gateway", lambda cfg: None)
|
||||
return free_tier_calls
|
||||
|
||||
def test_skip_keep_current_preserves_provider_and_model(self, tmp_path, monkeypatch):
|
||||
"""User picks Skip → config.yaml untouched, Nous creds still saved."""
|
||||
|
|
@ -1070,7 +1113,7 @@ class TestLoginNousSkipKeepsCurrent:
|
|||
hermes_home, config_path, auth_path = self._setup_home_with_openrouter(
|
||||
tmp_path, monkeypatch,
|
||||
)
|
||||
self._patch_login_internals(
|
||||
free_tier_calls = self._patch_login_internals(
|
||||
monkeypatch, prompt_returns="xiaomi/mimo-v2-pro",
|
||||
)
|
||||
|
||||
|
|
@ -1083,6 +1126,7 @@ class TestLoginNousSkipKeepsCurrent:
|
|||
cfg_after = yaml.safe_load(config_path.read_text())
|
||||
assert cfg_after["model"]["provider"] == "nous"
|
||||
assert cfg_after["model"]["default"] == "xiaomi/mimo-v2-pro"
|
||||
assert free_tier_calls == [{"force_fresh": True}]
|
||||
|
||||
auth_after = json.loads(auth_path.read_text())
|
||||
assert auth_after["active_provider"] == "nous"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from hermes_cli.nous_account import NousPortalAccountInfo
|
||||
from hermes_cli.models import (
|
||||
OPENROUTER_MODELS, fetch_openrouter_models, model_ids, detect_provider_for_model,
|
||||
is_nous_free_tier, partition_nous_models_by_tier,
|
||||
|
|
@ -308,6 +309,15 @@ class TestDetectProviderForModel:
|
|||
class TestIsNousFreeTier:
|
||||
"""Tests for is_nous_free_tier — account tier detection."""
|
||||
|
||||
def test_paid_service_access_allowed_true_is_not_free(self):
|
||||
assert is_nous_free_tier({"paid_service_access": {"allowed": True}}) is False
|
||||
|
||||
def test_paid_service_access_allowed_false_is_free(self):
|
||||
assert is_nous_free_tier({"paid_service_access": {"allowed": False}}) is True
|
||||
|
||||
def test_paid_service_access_paid_access_fallback(self):
|
||||
assert is_nous_free_tier({"paid_service_access": {"paid_access": False}}) is True
|
||||
|
||||
def test_paid_plus_tier(self):
|
||||
assert is_nous_free_tier({"subscription": {"plan": "Plus", "tier": 2, "monthly_charge": 20}}) is False
|
||||
|
||||
|
|
@ -657,39 +667,58 @@ class TestCheckNousFreeTierCache:
|
|||
def teardown_method(self):
|
||||
_models_mod._free_tier_cache = None
|
||||
|
||||
@patch("hermes_cli.models.fetch_nous_account_tier")
|
||||
@patch("hermes_cli.models.is_nous_free_tier", return_value=True)
|
||||
def test_result_is_cached(self, mock_is_free, mock_fetch):
|
||||
"""Second call within TTL returns cached result without API call."""
|
||||
mock_fetch.return_value = {"subscription": {"monthly_charge": 0}}
|
||||
with patch("hermes_cli.auth.get_provider_auth_state", return_value={"access_token": "tok"}), \
|
||||
patch("hermes_cli.auth.resolve_nous_runtime_credentials"):
|
||||
result1 = check_nous_free_tier()
|
||||
result2 = check_nous_free_tier()
|
||||
@patch("hermes_cli.nous_account.get_nous_portal_account_info")
|
||||
def test_result_is_cached(self, mock_account):
|
||||
"""Second call within TTL returns cached result without account lookup."""
|
||||
mock_account.return_value = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="jwt",
|
||||
fresh=False,
|
||||
paid_service_access=False,
|
||||
)
|
||||
result1 = check_nous_free_tier()
|
||||
result2 = check_nous_free_tier()
|
||||
|
||||
assert result1 is True
|
||||
assert result2 is True
|
||||
assert mock_fetch.call_count == 1
|
||||
assert mock_account.call_count == 1
|
||||
|
||||
@patch("hermes_cli.models.fetch_nous_account_tier")
|
||||
@patch("hermes_cli.models.is_nous_free_tier", return_value=False)
|
||||
def test_cache_expires_after_ttl(self, mock_is_free, mock_fetch):
|
||||
"""After TTL expires, the API is called again."""
|
||||
mock_fetch.return_value = {"subscription": {"monthly_charge": 20}}
|
||||
with patch("hermes_cli.auth.get_provider_auth_state", return_value={"access_token": "tok"}), \
|
||||
patch("hermes_cli.auth.resolve_nous_runtime_credentials"):
|
||||
result1 = check_nous_free_tier()
|
||||
assert mock_fetch.call_count == 1
|
||||
@patch("hermes_cli.nous_account.get_nous_portal_account_info")
|
||||
def test_cache_expires_after_ttl(self, mock_account):
|
||||
"""After TTL expires, account info is resolved again."""
|
||||
mock_account.return_value = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="jwt",
|
||||
fresh=False,
|
||||
paid_service_access=True,
|
||||
)
|
||||
result1 = check_nous_free_tier()
|
||||
assert mock_account.call_count == 1
|
||||
|
||||
cached_result, cached_at = _models_mod._free_tier_cache
|
||||
_models_mod._free_tier_cache = (cached_result, cached_at - _FREE_TIER_CACHE_TTL - 1)
|
||||
cached_result, cached_at = _models_mod._free_tier_cache
|
||||
_models_mod._free_tier_cache = (cached_result, cached_at - _FREE_TIER_CACHE_TTL - 1)
|
||||
|
||||
result2 = check_nous_free_tier()
|
||||
assert mock_fetch.call_count == 2
|
||||
result2 = check_nous_free_tier()
|
||||
assert mock_account.call_count == 2
|
||||
|
||||
assert result1 is False
|
||||
assert result2 is False
|
||||
|
||||
@patch("hermes_cli.nous_account.get_nous_portal_account_info")
|
||||
def test_force_fresh_bypasses_cache(self, mock_account):
|
||||
mock_account.return_value = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="account_api",
|
||||
fresh=True,
|
||||
paid_service_access=True,
|
||||
)
|
||||
|
||||
assert check_nous_free_tier() is False
|
||||
assert check_nous_free_tier(force_fresh=True) is False
|
||||
|
||||
assert mock_account.call_count == 2
|
||||
mock_account.assert_called_with(force_fresh=True)
|
||||
|
||||
def test_cache_ttl_is_short(self):
|
||||
"""TTL should be short enough to catch upgrades quickly (<=5 min)."""
|
||||
assert _FREE_TIER_CACHE_TTL <= 300
|
||||
|
|
|
|||
547
tests/hermes_cli/test_nous_account.py
Normal file
547
tests/hermes_cli/test_nous_account.py
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
"""Tests for normalized Nous Portal account entitlement helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.nous_account import (
|
||||
NousPaidServiceAccessInfo,
|
||||
NousPortalAccountInfo,
|
||||
format_nous_portal_entitlement_message,
|
||||
get_nous_portal_account_info,
|
||||
reset_nous_portal_account_info_cache,
|
||||
)
|
||||
|
||||
|
||||
def _jwt(claims: dict[str, Any]) -> str:
|
||||
def _part(payload: dict[str, Any]) -> str:
|
||||
raw = json.dumps(payload, separators=(",", ":")).encode()
|
||||
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
return f"{_part({'alg': 'none', 'typ': 'JWT'})}.{_part(claims)}.sig"
|
||||
|
||||
|
||||
def _state(token: str) -> dict[str, Any]:
|
||||
return {
|
||||
"access_token": token,
|
||||
"portal_base_url": "https://portal.example.test",
|
||||
"client_id": "hermes-cli",
|
||||
}
|
||||
|
||||
|
||||
def _account_payload(
|
||||
*,
|
||||
allowed: bool,
|
||||
subscription: dict[str, Any] | None,
|
||||
subscription_credits: float,
|
||||
purchased_credits: float,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"user": {
|
||||
"email": "alice@example.test",
|
||||
"privy_did": "did:privy:alice",
|
||||
},
|
||||
"organisation": {
|
||||
"id": "org_123",
|
||||
},
|
||||
"subscription": subscription,
|
||||
"purchased_credits_remaining": purchased_credits,
|
||||
"paid_service_access": {
|
||||
"allowed": allowed,
|
||||
"paid_access": allowed,
|
||||
"reason": "usable_credits" if allowed else "no_usable_credits",
|
||||
"organisation_id": "org_123",
|
||||
"effective_at_ms": 123456789,
|
||||
"has_active_subscription": subscription is not None,
|
||||
"active_subscription_is_paid": bool(
|
||||
subscription and subscription.get("monthly_charge", 0) > 0
|
||||
),
|
||||
"subscription_tier": subscription.get("tier") if subscription else None,
|
||||
"subscription_monthly_charge": (
|
||||
subscription.get("monthly_charge") if subscription else None
|
||||
),
|
||||
"subscription_credits_remaining": subscription_credits,
|
||||
"purchased_credits_remaining": purchased_credits,
|
||||
"total_usable_credits": subscription_credits + purchased_credits,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_cache():
|
||||
reset_nous_portal_account_info_cache()
|
||||
yield
|
||||
reset_nous_portal_account_info_cache()
|
||||
|
||||
|
||||
def test_valid_jwt_with_paid_access_true(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"client_id": "hermes-cli",
|
||||
"product_id": "nous-hermes-agent",
|
||||
"nous_client": "hermes-agent",
|
||||
"exp": int(time.time()) + 900,
|
||||
"paid_access": True,
|
||||
"subscription_tier": 2,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: _state(token))
|
||||
|
||||
info = get_nous_portal_account_info()
|
||||
|
||||
assert info.source == "jwt"
|
||||
assert info.fresh is False
|
||||
assert info.logged_in is True
|
||||
assert info.user_id == "user_123"
|
||||
assert info.org_id == "org_123"
|
||||
assert info.product_id == "nous-hermes-agent"
|
||||
assert info.paid_service_access is True
|
||||
assert info.is_paid is True
|
||||
assert info.is_free_tier is False
|
||||
|
||||
|
||||
def test_valid_jwt_with_paid_access_false(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"exp": int(time.time()) + 900,
|
||||
"paid_access": False,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: _state(token))
|
||||
|
||||
info = get_nous_portal_account_info()
|
||||
|
||||
assert info.source == "jwt"
|
||||
assert info.paid_service_access is False
|
||||
assert info.is_paid is False
|
||||
assert info.is_free_tier is True
|
||||
|
||||
|
||||
def test_valid_jwt_missing_paid_access_is_unknown_not_paid(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"exp": int(time.time()) + 900,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: _state(token))
|
||||
|
||||
info = get_nous_portal_account_info()
|
||||
|
||||
assert info.source == "jwt"
|
||||
assert info.paid_service_access is None
|
||||
assert info.is_paid is False
|
||||
assert info.is_free_tier is False
|
||||
|
||||
|
||||
def test_expired_jwt_falls_back_to_fresh_account(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"exp": int(time.time()) - 60,
|
||||
"paid_access": False,
|
||||
}
|
||||
)
|
||||
payload = _account_payload(
|
||||
allowed=True,
|
||||
subscription={
|
||||
"plan": "Tier 2",
|
||||
"tier": 2,
|
||||
"monthly_charge": 20,
|
||||
"current_period_end": "2026-05-01T00:00:00.000Z",
|
||||
"credits_remaining": 12.25,
|
||||
"rollover_credits": 3.5,
|
||||
},
|
||||
subscription_credits=12.25,
|
||||
purchased_credits=7.75,
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: _state(token))
|
||||
monkeypatch.setattr("hermes_cli.auth.resolve_nous_access_token", lambda: "fresh-token")
|
||||
monkeypatch.setattr("hermes_cli.nous_account._fetch_nous_account_info", lambda *a, **kw: payload)
|
||||
|
||||
info = get_nous_portal_account_info()
|
||||
|
||||
assert info.source == "account_api"
|
||||
assert info.fresh is True
|
||||
assert info.paid_service_access is True
|
||||
assert info.subscription is not None
|
||||
assert info.subscription.monthly_charge == 20
|
||||
assert info.paid_service_access_info is not None
|
||||
assert info.paid_service_access_info.total_usable_credits == 20
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "expected_paid"),
|
||||
[
|
||||
(
|
||||
_account_payload(
|
||||
allowed=True,
|
||||
subscription={
|
||||
"plan": "Tier 2",
|
||||
"tier": 2,
|
||||
"monthly_charge": 20,
|
||||
"current_period_end": "2026-05-01T00:00:00.000Z",
|
||||
"credits_remaining": 12.25,
|
||||
"rollover_credits": 3.5,
|
||||
},
|
||||
subscription_credits=12.25,
|
||||
purchased_credits=7.75,
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
_account_payload(
|
||||
allowed=False,
|
||||
subscription={
|
||||
"plan": "Tier 2",
|
||||
"tier": 2,
|
||||
"monthly_charge": 20,
|
||||
"current_period_end": "2026-05-01T00:00:00.000Z",
|
||||
"credits_remaining": 0,
|
||||
"rollover_credits": 0,
|
||||
},
|
||||
subscription_credits=0,
|
||||
purchased_credits=0,
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
_account_payload(
|
||||
allowed=True,
|
||||
subscription=None,
|
||||
subscription_credits=0,
|
||||
purchased_credits=7.75,
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
_account_payload(
|
||||
allowed=False,
|
||||
subscription=None,
|
||||
subscription_credits=0,
|
||||
purchased_credits=0,
|
||||
),
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_fresh_account_payload_normalization(monkeypatch, payload, expected_paid):
|
||||
token = _jwt({"sub": "user_123", "org_id": "org_123", "exp": int(time.time()) + 900})
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: _state(token))
|
||||
monkeypatch.setattr("hermes_cli.auth.resolve_nous_access_token", lambda: "fresh-token")
|
||||
monkeypatch.setattr("hermes_cli.nous_account._fetch_nous_account_info", lambda *a, **kw: payload)
|
||||
|
||||
info = get_nous_portal_account_info(force_fresh=True)
|
||||
|
||||
assert isinstance(info, NousPortalAccountInfo)
|
||||
assert info.source == "account_api"
|
||||
assert info.fresh is True
|
||||
assert info.email == "alice@example.test"
|
||||
assert info.privy_did == "did:privy:alice"
|
||||
assert info.org_id == "org_123"
|
||||
assert info.paid_service_access is expected_paid
|
||||
assert info.is_paid is expected_paid
|
||||
assert info.is_free_tier is (not expected_paid)
|
||||
|
||||
|
||||
def test_force_fresh_uses_account_api_even_when_jwt_is_valid(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"exp": int(time.time()) + 900,
|
||||
"paid_access": False,
|
||||
}
|
||||
)
|
||||
payload = _account_payload(
|
||||
allowed=True,
|
||||
subscription=None,
|
||||
subscription_credits=0,
|
||||
purchased_credits=5,
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: _state(token))
|
||||
monkeypatch.setattr("hermes_cli.auth.resolve_nous_access_token", lambda: "fresh-token")
|
||||
monkeypatch.setattr("hermes_cli.nous_account._fetch_nous_account_info", lambda *a, **kw: payload)
|
||||
|
||||
info = get_nous_portal_account_info(force_fresh=True)
|
||||
|
||||
assert info.source == "account_api"
|
||||
assert info.paid_service_access is True
|
||||
|
||||
|
||||
def test_no_oauth_token_reports_inference_key_present(monkeypatch):
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: {})
|
||||
|
||||
class _Entry:
|
||||
label = "manual-nous"
|
||||
access_token = ""
|
||||
agent_key = "opaque-runtime-key"
|
||||
agent_key_expires_at = "2099-01-01T00:00:00+00:00"
|
||||
expires_at = None
|
||||
inference_base_url = "https://inference.example.test/v1"
|
||||
base_url = "https://inference.example.test/v1"
|
||||
priority = 0
|
||||
|
||||
@property
|
||||
def runtime_api_key(self):
|
||||
return self.agent_key
|
||||
|
||||
@property
|
||||
def runtime_base_url(self):
|
||||
return self.inference_base_url
|
||||
|
||||
class _Pool:
|
||||
def has_credentials(self):
|
||||
return True
|
||||
|
||||
def entries(self):
|
||||
return [_Entry()]
|
||||
|
||||
monkeypatch.setattr("agent.credential_pool.load_pool", lambda provider: _Pool())
|
||||
|
||||
info = get_nous_portal_account_info()
|
||||
|
||||
assert info.logged_in is False
|
||||
assert info.source == "inference_key"
|
||||
assert info.inference_credential_present is True
|
||||
assert info.credential_source == "pool:manual-nous"
|
||||
assert info.paid_service_access is None
|
||||
|
||||
|
||||
def test_pool_oauth_entry_uses_jwt_snapshot(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"client_id": "hermes-cli",
|
||||
"exp": int(time.time()) + 900,
|
||||
"paid_access": True,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: {})
|
||||
|
||||
class _Entry:
|
||||
label = "dashboard device_code"
|
||||
auth_type = "oauth"
|
||||
access_token = token
|
||||
refresh_token = "refresh-token"
|
||||
agent_key = "opaque-runtime-key"
|
||||
agent_key_expires_at = "2099-01-01T00:00:00+00:00"
|
||||
expires_at = "2099-01-01T00:00:00+00:00"
|
||||
portal_base_url = "https://portal.example.test"
|
||||
inference_base_url = "https://inference.example.test/v1"
|
||||
base_url = "https://inference.example.test/v1"
|
||||
priority = 0
|
||||
|
||||
@property
|
||||
def runtime_api_key(self):
|
||||
return self.agent_key
|
||||
|
||||
@property
|
||||
def runtime_base_url(self):
|
||||
return self.inference_base_url
|
||||
|
||||
class _Pool:
|
||||
def has_credentials(self):
|
||||
return True
|
||||
|
||||
def entries(self):
|
||||
return [_Entry()]
|
||||
|
||||
monkeypatch.setattr("agent.credential_pool.load_pool", lambda provider: _Pool())
|
||||
|
||||
info = get_nous_portal_account_info()
|
||||
|
||||
assert info.logged_in is True
|
||||
assert info.source == "jwt"
|
||||
assert info.paid_service_access is True
|
||||
assert info.credential_source == "pool:dashboard device_code"
|
||||
|
||||
|
||||
def test_pool_oauth_entry_force_fresh_uses_account_api(monkeypatch):
|
||||
token = _jwt(
|
||||
{
|
||||
"sub": "user_123",
|
||||
"org_id": "org_123",
|
||||
"exp": int(time.time()) + 900,
|
||||
"paid_access": False,
|
||||
}
|
||||
)
|
||||
payload = _account_payload(
|
||||
allowed=True,
|
||||
subscription=None,
|
||||
subscription_credits=0,
|
||||
purchased_credits=3,
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider: {})
|
||||
monkeypatch.setattr("hermes_cli.nous_account._fetch_nous_account_info", lambda *a, **kw: payload)
|
||||
|
||||
class _Entry:
|
||||
label = "dashboard device_code"
|
||||
auth_type = "oauth"
|
||||
access_token = token
|
||||
refresh_token = "refresh-token"
|
||||
agent_key = "opaque-runtime-key"
|
||||
agent_key_expires_at = "2099-01-01T00:00:00+00:00"
|
||||
expires_at = "2099-01-01T00:00:00+00:00"
|
||||
portal_base_url = "https://portal.example.test"
|
||||
inference_base_url = "https://inference.example.test/v1"
|
||||
base_url = "https://inference.example.test/v1"
|
||||
priority = 0
|
||||
|
||||
@property
|
||||
def runtime_api_key(self):
|
||||
return self.agent_key
|
||||
|
||||
@property
|
||||
def runtime_base_url(self):
|
||||
return self.inference_base_url
|
||||
|
||||
class _Pool:
|
||||
def has_credentials(self):
|
||||
return True
|
||||
|
||||
def entries(self):
|
||||
return [_Entry()]
|
||||
|
||||
monkeypatch.setattr("agent.credential_pool.load_pool", lambda provider: _Pool())
|
||||
|
||||
info = get_nous_portal_account_info(force_fresh=True)
|
||||
|
||||
assert info.logged_in is True
|
||||
assert info.source == "account_api"
|
||||
assert info.fresh is True
|
||||
assert info.paid_service_access is True
|
||||
assert info.credential_source == "pool:dashboard device_code"
|
||||
|
||||
|
||||
def test_entitlement_message_returns_none_for_paid_access():
|
||||
info = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="account_api",
|
||||
fresh=True,
|
||||
paid_service_access=True,
|
||||
portal_base_url="https://portal.example.test",
|
||||
)
|
||||
|
||||
assert format_nous_portal_entitlement_message(info, capability="paid models") is None
|
||||
|
||||
|
||||
def test_entitlement_message_for_inference_key_without_portal_login():
|
||||
info = NousPortalAccountInfo(
|
||||
logged_in=False,
|
||||
source="inference_key",
|
||||
fresh=False,
|
||||
inference_credential_present=True,
|
||||
portal_base_url="https://portal.example.test",
|
||||
)
|
||||
|
||||
message = format_nous_portal_entitlement_message(
|
||||
info,
|
||||
capability="managed tools",
|
||||
)
|
||||
|
||||
assert message is not None
|
||||
assert "Nous inference credentials are configured" in message
|
||||
assert "cannot verify your Nous Portal paid access" in message
|
||||
assert "Log in with `hermes model`" in message
|
||||
|
||||
|
||||
def test_entitlement_message_for_active_paid_subscription_with_no_credits():
|
||||
info = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="account_api",
|
||||
fresh=True,
|
||||
paid_service_access=False,
|
||||
portal_base_url="https://portal.example.test",
|
||||
paid_service_access_info=NousPaidServiceAccessInfo(
|
||||
allowed=False,
|
||||
reason="no_usable_credits",
|
||||
has_active_subscription=True,
|
||||
active_subscription_is_paid=True,
|
||||
subscription_credits_remaining=0,
|
||||
purchased_credits_remaining=0,
|
||||
total_usable_credits=0,
|
||||
),
|
||||
)
|
||||
|
||||
message = format_nous_portal_entitlement_message(
|
||||
info,
|
||||
capability="managed tools",
|
||||
)
|
||||
|
||||
assert message is not None
|
||||
assert "credits are exhausted" in message
|
||||
assert "managed tools" in message
|
||||
assert "https://portal.example.test/billing" in message
|
||||
|
||||
|
||||
def test_entitlement_message_for_no_subscription_or_credits():
|
||||
info = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="account_api",
|
||||
fresh=True,
|
||||
paid_service_access=False,
|
||||
portal_base_url="https://portal.example.test",
|
||||
paid_service_access_info=NousPaidServiceAccessInfo(
|
||||
allowed=False,
|
||||
reason="no_usable_credits",
|
||||
has_active_subscription=False,
|
||||
subscription_credits_remaining=0,
|
||||
purchased_credits_remaining=0,
|
||||
total_usable_credits=0,
|
||||
),
|
||||
)
|
||||
|
||||
message = format_nous_portal_entitlement_message(info, capability="paid models")
|
||||
|
||||
assert message is not None
|
||||
assert "no active subscription or usable credits" in message
|
||||
assert "Subscribe or add credits" in message
|
||||
|
||||
|
||||
def test_entitlement_message_for_unknown_entitlement_is_explicit():
|
||||
info = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="error",
|
||||
fresh=False,
|
||||
paid_service_access=None,
|
||||
portal_base_url="https://portal.example.test",
|
||||
error="account_api_timeout",
|
||||
)
|
||||
|
||||
message = format_nous_portal_entitlement_message(info, capability="Tool Gateway")
|
||||
|
||||
assert message is not None
|
||||
assert "could not verify" in message
|
||||
assert "account_api_timeout" in message
|
||||
assert "Run `hermes model`" in message
|
||||
|
||||
|
||||
def test_entitlement_message_for_account_missing():
|
||||
info = NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="account_api",
|
||||
fresh=True,
|
||||
paid_service_access=False,
|
||||
paid_service_access_info=NousPaidServiceAccessInfo(
|
||||
allowed=False,
|
||||
reason="account_missing",
|
||||
),
|
||||
)
|
||||
|
||||
message = format_nous_portal_entitlement_message(info, capability="Tool Gateway")
|
||||
|
||||
assert message is not None
|
||||
assert "could not find a Nous Portal account or organisation" in message
|
||||
|
|
@ -1,14 +1,25 @@
|
|||
"""Tests for Nous subscription feature detection."""
|
||||
|
||||
from hermes_cli.nous_account import NousPortalAccountInfo
|
||||
from hermes_cli import nous_subscription as ns
|
||||
|
||||
|
||||
def _account(*, logged_in: bool, paid: bool | None = None) -> NousPortalAccountInfo:
|
||||
return NousPortalAccountInfo(
|
||||
logged_in=logged_in,
|
||||
source="jwt" if logged_in else "none",
|
||||
fresh=False,
|
||||
paid_service_access=paid,
|
||||
)
|
||||
|
||||
|
||||
def test_get_nous_subscription_features_recognizes_direct_exa_backend(monkeypatch):
|
||||
env = {"EXA_API_KEY": "exa-test"}
|
||||
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=False)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "web")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
@ -26,8 +37,9 @@ def test_get_nous_subscription_features_recognizes_direct_exa_backend(monkeypatc
|
|||
def test_get_nous_subscription_features_prefers_managed_modal_in_auto_mode(monkeypatch):
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: "")
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "terminal")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
@ -46,8 +58,9 @@ def test_get_nous_subscription_features_prefers_managed_modal_in_auto_mode(monke
|
|||
|
||||
def test_get_nous_subscription_features_marks_browser_use_as_managed_when_gateway_ready(monkeypatch):
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: "")
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: True)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
@ -78,8 +91,9 @@ def test_get_nous_subscription_features_uses_direct_browserbase_when_no_managed_
|
|||
}
|
||||
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: True)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
@ -103,8 +117,9 @@ def test_get_nous_subscription_features_prefers_camofox_over_managed_browser_use
|
|||
env = {"CAMOFOX_URL": "http://localhost:9377"}
|
||||
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
@ -133,8 +148,9 @@ def test_get_nous_subscription_features_requires_agent_browser_for_browserbase(m
|
|||
}
|
||||
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=False)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
@ -155,8 +171,9 @@ def test_get_nous_subscription_features_does_not_treat_quoted_false_as_gateway_o
|
|||
env = {"EXA_API_KEY": "exa-test"}
|
||||
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
||||
)
|
||||
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "web")
|
||||
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
||||
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
||||
|
|
|
|||
|
|
@ -83,6 +83,87 @@ def test_show_status_reports_nous_auth_error(monkeypatch, capsys, tmp_path):
|
|||
assert "Key exp:" in output
|
||||
|
||||
|
||||
def test_show_status_reports_nous_inference_key_without_portal_login(monkeypatch, capsys, tmp_path):
|
||||
from hermes_cli import status as status_mod
|
||||
from hermes_cli.nous_account import NousPortalAccountInfo
|
||||
import hermes_cli.auth as auth_mod
|
||||
import hermes_cli.gateway as gateway_mod
|
||||
|
||||
monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
|
||||
monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
|
||||
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False)
|
||||
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False)
|
||||
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False)
|
||||
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False)
|
||||
monkeypatch.setattr(
|
||||
auth_mod,
|
||||
"get_nous_auth_status",
|
||||
lambda: {
|
||||
"logged_in": False,
|
||||
"inference_credential_present": True,
|
||||
"credential_source": "pool:manual opaque key",
|
||||
"inference_base_url": "https://inference.example.com/v1",
|
||||
"agent_key_expires_at": "2099-01-01T00:00:00+00:00",
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_mod,
|
||||
"get_nous_portal_account_info",
|
||||
lambda: NousPortalAccountInfo(
|
||||
logged_in=False,
|
||||
source="inference_key",
|
||||
fresh=False,
|
||||
inference_credential_present=True,
|
||||
inference_base_url="https://inference.example.com/v1",
|
||||
),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(status_mod, "managed_nous_tools_enabled", lambda: False, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_qwen_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False)
|
||||
|
||||
status_mod.show_status(SimpleNamespace(all=False, deep=False))
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Nous Portal ✗ not logged in (Nous inference key configured)" in output
|
||||
assert "Inference: https://inference.example.com/v1" in output
|
||||
assert "Nous inference credentials are configured" in output
|
||||
|
||||
|
||||
def test_show_status_reports_vercel_backend_contract(monkeypatch, capsys, tmp_path):
|
||||
from hermes_cli import status as status_mod
|
||||
import hermes_cli.auth as auth_mod
|
||||
import hermes_cli.gateway as gateway_mod
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("TERMINAL_ENV", "vercel_sandbox")
|
||||
monkeypatch.setenv("TERMINAL_VERCEL_RUNTIME", "python3.13")
|
||||
monkeypatch.setenv("TERMINAL_CONTAINER_PERSISTENT", "true")
|
||||
monkeypatch.setenv("VERCEL_OIDC_TOKEN", "oidc-token")
|
||||
monkeypatch.setattr(status_mod.importlib.util, "find_spec", lambda name: object() if name == "vercel" else None)
|
||||
monkeypatch.setattr(status_mod, "load_config", lambda: {"terminal": {"backend": "vercel_sandbox"}}, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_qwen_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status", lambda: {}, raising=False)
|
||||
monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False)
|
||||
|
||||
status_mod.show_status(SimpleNamespace(all=False, deep=False))
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Backend: vercel_sandbox" in output
|
||||
assert "Runtime: python3.13" in output
|
||||
assert "Auth:" in output and "OIDC token via VERCEL_OIDC_TOKEN" in output
|
||||
assert "Auth detail: mode: OIDC" in output
|
||||
assert "Auth detail: active env: VERCEL_OIDC_TOKEN" in output
|
||||
assert "oidc-token" not in output
|
||||
assert "snapshot filesystem" in output
|
||||
assert "live processes do not survive" in output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers shared by xAI OAuth status tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from hermes_cli.nous_account import NousPaidServiceAccessInfo, NousPortalAccountInfo
|
||||
from hermes_cli.nous_subscription import NousFeatureState, NousSubscriptionFeatures
|
||||
|
||||
|
||||
|
|
@ -124,6 +125,59 @@ def test_show_status_hides_nous_subscription_section_when_feature_flag_is_off(mo
|
|||
assert "Nous Tool Gateway" not in out
|
||||
|
||||
|
||||
def test_show_status_reports_exhausted_nous_credits(monkeypatch, capsys, tmp_path):
|
||||
monkeypatch.setattr("hermes_cli.status.managed_nous_tools_enabled", lambda: False)
|
||||
from hermes_cli import status as status_mod
|
||||
import hermes_cli.auth as auth_mod
|
||||
|
||||
_patch_common_status_deps(monkeypatch, status_mod, tmp_path)
|
||||
monkeypatch.setattr(
|
||||
auth_mod,
|
||||
"get_nous_auth_status",
|
||||
lambda: {
|
||||
"logged_in": False,
|
||||
"access_token": "jwt",
|
||||
"portal_base_url": "https://portal.example.test",
|
||||
"error": "credits exhausted",
|
||||
"error_code": "insufficient_credits",
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_mod,
|
||||
"get_nous_portal_account_info",
|
||||
lambda: NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="account_api",
|
||||
fresh=True,
|
||||
paid_service_access=False,
|
||||
portal_base_url="https://portal.example.test",
|
||||
paid_service_access_info=NousPaidServiceAccessInfo(
|
||||
allowed=False,
|
||||
reason="no_usable_credits",
|
||||
has_active_subscription=True,
|
||||
active_subscription_is_paid=True,
|
||||
subscription_credits_remaining=0,
|
||||
purchased_credits_remaining=0,
|
||||
total_usable_credits=0,
|
||||
),
|
||||
),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": {"provider": "nous"}}, raising=False)
|
||||
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "nous", raising=False)
|
||||
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "nous", raising=False)
|
||||
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "Nous Portal", raising=False)
|
||||
|
||||
status_mod.show_status(SimpleNamespace(all=False, deep=False))
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Nous Tool Gateway" in out
|
||||
assert "credits are exhausted" in out
|
||||
assert "https://portal.example.test/billing" in out
|
||||
assert "free-tier Nous account" not in out
|
||||
|
||||
|
||||
def test_show_status_reports_empty_lmstudio_listing_as_reachable(monkeypatch, capsys, tmp_path):
|
||||
from hermes_cli import status as status_mod
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.nous_account import NousPortalAccountInfo
|
||||
from hermes_cli.tools_config import (
|
||||
_DEFAULT_OFF_TOOLSETS,
|
||||
_apply_toolset_change,
|
||||
|
|
@ -557,8 +558,13 @@ def test_visible_providers_include_nous_subscription_when_logged_in(monkeypatch)
|
|||
config = {"model": {"provider": "nous"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
"hermes_cli.nous_subscription.get_nous_portal_account_info",
|
||||
lambda: NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="jwt",
|
||||
fresh=False,
|
||||
paid_service_access=True,
|
||||
),
|
||||
)
|
||||
|
||||
providers = _visible_providers(TOOL_CATEGORIES["browser"], config)
|
||||
|
|
@ -571,8 +577,13 @@ def test_visible_providers_hide_nous_subscription_when_feature_flag_is_off(monke
|
|||
config = {"model": {"provider": "nous"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
"hermes_cli.nous_subscription.get_nous_portal_account_info",
|
||||
lambda: NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="jwt",
|
||||
fresh=False,
|
||||
paid_service_access=True,
|
||||
),
|
||||
)
|
||||
|
||||
providers = _visible_providers(TOOL_CATEGORIES["browser"], config)
|
||||
|
|
@ -657,8 +668,13 @@ def test_first_install_nous_auto_configures_managed_defaults(monkeypatch):
|
|||
lambda: ["cli"],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
"hermes_cli.nous_subscription.get_nous_portal_account_info",
|
||||
lambda: NousPortalAccountInfo(
|
||||
logged_in=True,
|
||||
source="jwt",
|
||||
fresh=False,
|
||||
paid_service_access=True,
|
||||
),
|
||||
)
|
||||
|
||||
configured = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue