mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
207 lines
6.4 KiB
Python
207 lines
6.4 KiB
Python
"""Tests for Qwen OAuth provider authentication (hermes_cli/auth.py).
|
|
|
|
Covers: _qwen_cli_auth_path, _read_qwen_cli_tokens, _save_qwen_cli_tokens,
|
|
_qwen_access_token_is_expiring, _refresh_qwen_cli_tokens,
|
|
resolve_qwen_runtime_credentials, get_qwen_auth_status.
|
|
"""
|
|
|
|
import json
|
|
import stat
|
|
import time
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.auth import (
|
|
AuthError,
|
|
DEFAULT_QWEN_BASE_URL,
|
|
QWEN_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
|
|
_qwen_cli_auth_path,
|
|
_read_qwen_cli_tokens,
|
|
_save_qwen_cli_tokens,
|
|
_qwen_access_token_is_expiring,
|
|
_refresh_qwen_cli_tokens,
|
|
resolve_qwen_runtime_credentials,
|
|
get_qwen_auth_status,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _make_qwen_tokens(
|
|
access_token="test-access-token",
|
|
refresh_token="test-refresh-token",
|
|
expiry_date=None,
|
|
**extra,
|
|
):
|
|
"""Create a minimal Qwen CLI OAuth credential dict."""
|
|
if expiry_date is None:
|
|
# 1 hour from now in milliseconds
|
|
expiry_date = int((time.time() + 3600) * 1000)
|
|
data = {
|
|
"access_token": access_token,
|
|
"refresh_token": refresh_token,
|
|
"token_type": "Bearer",
|
|
"expiry_date": expiry_date,
|
|
"resource_url": "portal.qwen.ai",
|
|
}
|
|
data.update(extra)
|
|
return data
|
|
|
|
|
|
def _write_qwen_creds(tmp_path, tokens=None):
|
|
"""Write tokens to the Qwen CLI credentials file and return the path."""
|
|
qwen_dir = tmp_path / ".qwen"
|
|
qwen_dir.mkdir(parents=True, exist_ok=True)
|
|
creds_path = qwen_dir / "oauth_creds.json"
|
|
if tokens is None:
|
|
tokens = _make_qwen_tokens()
|
|
creds_path.write_text(json.dumps(tokens), encoding="utf-8")
|
|
return creds_path
|
|
|
|
|
|
@pytest.fixture()
|
|
def qwen_env(tmp_path, monkeypatch):
|
|
"""Redirect _qwen_cli_auth_path to tmp_path/.qwen/oauth_creds.json."""
|
|
creds_path = tmp_path / ".qwen" / "oauth_creds.json"
|
|
monkeypatch.setattr(
|
|
"hermes_cli.auth._qwen_cli_auth_path", lambda: creds_path
|
|
)
|
|
return tmp_path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _qwen_cli_auth_path
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_qwen_cli_auth_path_returns_expected_location():
|
|
path = _qwen_cli_auth_path()
|
|
assert path == Path.home() / ".qwen" / "oauth_creds.json"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _read_qwen_cli_tokens
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _save_qwen_cli_tokens
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _qwen_access_token_is_expiring
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _refresh_qwen_cli_tokens
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_qwen_runtime_credentials
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_resolve_qwen_runtime_credentials_fresh_token(qwen_env):
|
|
tokens = _make_qwen_tokens(access_token="fresh-at")
|
|
_write_qwen_creds(qwen_env, tokens)
|
|
|
|
creds = resolve_qwen_runtime_credentials(refresh_if_expiring=False)
|
|
assert creds["provider"] == "qwen-oauth"
|
|
assert creds["api_key"] == "fresh-at"
|
|
assert creds["base_url"] == DEFAULT_QWEN_BASE_URL
|
|
assert creds["source"] == "qwen-cli"
|
|
|
|
|
|
def test_resolve_qwen_runtime_credentials_missing_access_token(qwen_env):
|
|
tokens = _make_qwen_tokens(access_token="")
|
|
_write_qwen_creds(qwen_env, tokens)
|
|
|
|
with pytest.raises(AuthError) as exc:
|
|
resolve_qwen_runtime_credentials(refresh_if_expiring=False)
|
|
assert exc.value.code == "qwen_access_token_missing"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_qwen_auth_status
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_get_qwen_auth_status_logged_in(qwen_env):
|
|
tokens = _make_qwen_tokens(access_token="status-at")
|
|
_write_qwen_creds(qwen_env, tokens)
|
|
|
|
status = get_qwen_auth_status()
|
|
assert status["logged_in"] is True
|
|
assert status["api_key"] == "status-at"
|
|
|
|
|
|
def test_get_qwen_auth_status_refreshes_expired_token(qwen_env):
|
|
expired_ms = int((time.time() - 3600) * 1000)
|
|
tokens = _make_qwen_tokens(access_token="old-at", expiry_date=expired_ms)
|
|
_write_qwen_creds(qwen_env, tokens)
|
|
|
|
refreshed = _make_qwen_tokens(access_token="refreshed-at")
|
|
|
|
with patch(
|
|
"hermes_cli.auth._refresh_qwen_cli_tokens", return_value=refreshed
|
|
) as mock_refresh:
|
|
status = get_qwen_auth_status()
|
|
|
|
mock_refresh.assert_called_once()
|
|
assert status["logged_in"] is True
|
|
assert status["api_key"] == "refreshed-at"
|
|
|
|
|
|
def test_model_flow_qwen_oauth_stale_token_shows_reauth_guidance(qwen_env, monkeypatch, capsys):
|
|
from hermes_cli.main import _model_flow_qwen_oauth
|
|
|
|
expired_ms = int((time.time() - 3600) * 1000)
|
|
tokens = _make_qwen_tokens(access_token="dead-at", expiry_date=expired_ms)
|
|
_write_qwen_creds(qwen_env, tokens)
|
|
|
|
monkeypatch.setattr(
|
|
"hermes_cli.auth._refresh_qwen_cli_tokens",
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
AuthError(
|
|
"Qwen refresh rejected. Re-run 'qwen auth qwen-oauth'.",
|
|
provider="qwen-oauth",
|
|
code="qwen_refresh_failed",
|
|
)
|
|
),
|
|
)
|
|
|
|
prompt_called = {"value": False}
|
|
update_called = {"value": False}
|
|
|
|
monkeypatch.setattr(
|
|
"hermes_cli.auth._prompt_model_selection",
|
|
lambda *args, **kwargs: prompt_called.__setitem__("value", True),
|
|
)
|
|
monkeypatch.setattr(
|
|
"hermes_cli.auth._update_config_for_provider",
|
|
lambda *args, **kwargs: update_called.__setitem__("value", True),
|
|
)
|
|
|
|
_model_flow_qwen_oauth({}, current_model="qwen3-coder-plus")
|
|
|
|
out = capsys.readouterr().out
|
|
assert "Run: qwen auth qwen-oauth" in out
|
|
assert "Qwen refresh rejected" in out
|
|
assert prompt_called["value"] is False
|
|
assert update_called["value"] is False
|