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.
120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.auth import AuthError
|
|
from plugins.spotify import client as spotify_mod
|
|
from plugins.spotify import tools as spotify_tool
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, status_code: int, payload: dict | None = None, *, text: str = "", headers: dict | None = None):
|
|
self.status_code = status_code
|
|
self._payload = payload
|
|
self.text = text or (json.dumps(payload) if payload is not None else "")
|
|
self.headers = headers or {"content-type": "application/json"}
|
|
self.content = self.text.encode("utf-8") if self.text else b""
|
|
|
|
def json(self):
|
|
if self._payload is None:
|
|
raise ValueError("no json")
|
|
return self._payload
|
|
|
|
|
|
class _StubSpotifyClient:
|
|
def __init__(self, payload):
|
|
self.payload = payload
|
|
|
|
def get_currently_playing(self, *, market=None):
|
|
return self.payload
|
|
|
|
|
|
def test_spotify_client_retries_once_after_401(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
calls: list[str] = []
|
|
tokens = iter([
|
|
{
|
|
"access_token": "token-1",
|
|
"base_url": "https://api.spotify.com/v1",
|
|
},
|
|
{
|
|
"access_token": "token-2",
|
|
"base_url": "https://api.spotify.com/v1",
|
|
},
|
|
])
|
|
|
|
monkeypatch.setattr(
|
|
spotify_mod,
|
|
"resolve_spotify_runtime_credentials",
|
|
lambda **kwargs: next(tokens),
|
|
)
|
|
|
|
def fake_request(method, url, headers=None, params=None, json=None, timeout=None):
|
|
calls.append(headers["Authorization"])
|
|
if len(calls) == 1:
|
|
return _FakeResponse(401, {"error": {"message": "expired token"}})
|
|
return _FakeResponse(200, {"devices": [{"id": "dev-1"}]})
|
|
|
|
monkeypatch.setattr(spotify_mod.httpx, "request", fake_request)
|
|
|
|
client = spotify_mod.SpotifyClient()
|
|
payload = client.get_devices()
|
|
|
|
assert payload["devices"][0]["id"] == "dev-1"
|
|
assert calls == ["Bearer token-1", "Bearer token-2"]
|
|
|
|
|
|
def test_normalize_spotify_uri_accepts_urls() -> None:
|
|
uri = spotify_mod.normalize_spotify_uri(
|
|
"https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P",
|
|
"track",
|
|
)
|
|
assert uri == "spotify:track:7ouMYWpwJ422jRcDASZB7P"
|
|
|
|
|
|
def test_get_currently_playing_returns_explanatory_empty_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
spotify_mod,
|
|
"resolve_spotify_runtime_credentials",
|
|
lambda **kwargs: {
|
|
"access_token": "token-1",
|
|
"base_url": "https://api.spotify.com/v1",
|
|
},
|
|
)
|
|
|
|
def fake_request(method, url, headers=None, params=None, json=None, timeout=None):
|
|
return _FakeResponse(204, None, text="", headers={"content-type": "application/json"})
|
|
|
|
monkeypatch.setattr(spotify_mod.httpx, "request", fake_request)
|
|
|
|
client = spotify_mod.SpotifyClient()
|
|
payload = client.get_currently_playing()
|
|
|
|
assert payload == {
|
|
"status_code": 204,
|
|
"empty": True,
|
|
"message": "Spotify is not currently playing anything. Start playback in Spotify and try again.",
|
|
}
|
|
|
|
|
|
def test_client_wraps_invalid_grant_as_spotify_auth_required_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""SpotifyClient._resolve_runtime wraps AuthError(code=spotify_refresh_invalid_grant) into SpotifyAuthRequiredError."""
|
|
|
|
def _raise_invalid_grant(**kwargs):
|
|
raise AuthError(
|
|
"Spotify refresh token has expired or was revoked. Run `hermes auth spotify` again.",
|
|
provider="spotify",
|
|
code="spotify_refresh_invalid_grant",
|
|
relogin_required=True,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
spotify_mod,
|
|
"resolve_spotify_runtime_credentials",
|
|
_raise_invalid_grant,
|
|
)
|
|
with pytest.raises(spotify_mod.SpotifyAuthRequiredError, match="expired or was revoked"):
|
|
spotify_mod.SpotifyClient()
|