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.
137 lines
4.2 KiB
Python
137 lines
4.2 KiB
Python
"""Tests for the dashboard-auth cookie helpers."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import Response
|
|
from fastapi.testclient import TestClient
|
|
from starlette.requests import Request
|
|
|
|
from hermes_cli.dashboard_auth.cookies import (
|
|
PKCE_COOKIE,
|
|
SESSION_AT_COOKIE,
|
|
SESSION_PROVIDER_COOKIE,
|
|
SESSION_RT_COOKIE,
|
|
clear_pkce_cookie,
|
|
clear_session_cookies,
|
|
read_pkce_cookie,
|
|
read_session_cookies,
|
|
read_session_provider,
|
|
set_pkce_cookie,
|
|
set_session_cookies,
|
|
)
|
|
|
|
|
|
def _build_app(use_https: bool = True, prefix: str = ""):
|
|
app = FastAPI()
|
|
|
|
@app.get("/set")
|
|
def set_endpoint():
|
|
r = Response("ok")
|
|
set_session_cookies(
|
|
r, access_token="AT", refresh_token="RT",
|
|
access_token_expires_in=3600, use_https=use_https,
|
|
prefix=prefix, provider="nous",
|
|
)
|
|
return r
|
|
|
|
@app.get("/set-pkce")
|
|
def set_pkce():
|
|
r = Response("ok")
|
|
set_pkce_cookie(r, payload="provider=stub;state=s;verifier=v",
|
|
use_https=use_https, prefix=prefix)
|
|
return r
|
|
|
|
@app.get("/clear")
|
|
def clear():
|
|
r = Response("ok")
|
|
clear_session_cookies(r, prefix=prefix)
|
|
clear_pkce_cookie(r, prefix=prefix)
|
|
return r
|
|
|
|
return app
|
|
|
|
|
|
# Cookie name resolution helpers used throughout — the bare name resolves
|
|
# to a request-shape-dependent variant (__Host- / __Secure- / bare).
|
|
# Tests pin a specific shape so a regression in the name-resolution
|
|
# logic fails loudly rather than silently breaking sessions.
|
|
|
|
|
|
def test_session_cookies_use_host_prefix_on_https_direct():
|
|
"""HTTPS + no proxy prefix → __Host- prefix (strongest spec
|
|
hardening: bound to exact origin, requires Path=/, requires Secure)."""
|
|
client = TestClient(_build_app(use_https=True, prefix=""))
|
|
r = client.get("/set")
|
|
cookies = r.headers.get_list("set-cookie")
|
|
at = next(c for c in cookies if c.startswith(f"__Host-{SESSION_AT_COOKIE}="))
|
|
rt = next(c for c in cookies if c.startswith(f"__Host-{SESSION_RT_COOKIE}="))
|
|
provider = next(c for c in cookies if c.startswith(f"__Host-{SESSION_PROVIDER_COOKIE}=nous"))
|
|
for c in (at, rt, provider):
|
|
assert "HttpOnly" in c
|
|
assert "samesite=lax" in c.lower()
|
|
assert "Secure" in c
|
|
assert "Path=/" in c
|
|
|
|
|
|
def test_session_cookies_use_secure_prefix_when_proxied():
|
|
"""HTTPS + /hermes prefix → __Secure- prefix (__Host- forbids
|
|
Path != "/"; __Secure- keeps the Secure-required hardening)."""
|
|
client = TestClient(_build_app(use_https=True, prefix="/hermes"))
|
|
r = client.get("/set")
|
|
cookies = r.headers.get_list("set-cookie")
|
|
at = next(c for c in cookies if c.startswith(f"__Secure-{SESSION_AT_COOKIE}="))
|
|
assert "Path=/hermes" in at
|
|
assert "Secure" in at
|
|
# __Host- variant must NOT be emitted on the prefix path.
|
|
assert not any(
|
|
c.startswith(f"__Host-{SESSION_AT_COOKIE}=") for c in cookies
|
|
)
|
|
|
|
|
|
def test_session_cookies_use_bare_name_on_http():
|
|
"""Loopback HTTP dev: __Host- / __Secure- both require Secure, which
|
|
we can't set on HTTP. Use bare cookie names."""
|
|
client = TestClient(_build_app(use_https=False))
|
|
r = client.get("/set")
|
|
cookies = r.headers.get_list("set-cookie")
|
|
# Bare name present; no __Host- / __Secure- variant emitted.
|
|
assert any(c.startswith(f"{SESSION_AT_COOKIE}=") for c in cookies)
|
|
assert not any(
|
|
c.startswith(f"__Host-{SESSION_AT_COOKIE}=")
|
|
or c.startswith(f"__Secure-{SESSION_AT_COOKIE}=")
|
|
for c in cookies
|
|
)
|
|
# No Secure flag (HTTP).
|
|
at = next(c for c in cookies if c.startswith(f"{SESSION_AT_COOKIE}="))
|
|
assert "Secure" not in at
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_read_session_cookies_from_request_secure_prefix():
|
|
"""Reader also finds cookies set with the __Secure- variant
|
|
(HTTPS behind a proxy prefix)."""
|
|
scope = {
|
|
"type": "http",
|
|
"method": "GET",
|
|
"path": "/",
|
|
"headers": [(
|
|
b"cookie",
|
|
f"__Secure-{SESSION_AT_COOKIE}=at_value; "
|
|
f"__Secure-{SESSION_RT_COOKIE}=rt_value".encode(),
|
|
)],
|
|
}
|
|
req = Request(scope)
|
|
at, rt = read_session_cookies(req)
|
|
assert at == "at_value"
|
|
assert rt == "rt_value"
|
|
|
|
|
|
|
|
|