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.
152 lines
4.6 KiB
Python
152 lines
4.6 KiB
Python
"""E2E tests for the unified provider-credential lifecycle (#51071 #59761 #62269).
|
|
|
|
A provider API key can live in .env, auth.json's credential_pool, and
|
|
config.yaml mirrors at once. These tests drive the REAL dashboard endpoint
|
|
handlers (PUT/DELETE /api/env) against real on-disk fixtures in a temp
|
|
HERMES_HOME (tests/conftest.py isolation) and assert every store agrees
|
|
afterwards.
|
|
|
|
All fake secrets are constructed at runtime so no key-shaped literal ever
|
|
lands in the repo.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from hermes_cli.web_server import _SESSION_TOKEN, app
|
|
|
|
client = TestClient(app)
|
|
HEADERS = {"X-Hermes-Session-Token": _SESSION_TOKEN}
|
|
|
|
# Runtime-constructed fake credentials (never literal key-shaped strings).
|
|
FAKE_ZAI_KEY = "zk-" + "a" * 24
|
|
FAKE_OAUTH_TOKEN = "oa-" + "b" * 24
|
|
NEW_KEY = "zk-" + "c" * 24
|
|
|
|
|
|
@pytest.fixture
|
|
def hermes_home(monkeypatch, tmp_path):
|
|
"""Fresh HERMES_HOME with .env + auth.json + config.yaml fixtures."""
|
|
home = tmp_path / "cred_home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
from hermes_cli.config import invalidate_env_cache
|
|
|
|
invalidate_env_cache()
|
|
return home
|
|
|
|
|
|
def _write_env(home, **pairs):
|
|
home.joinpath(".env").write_text(
|
|
"".join(f"{k}={v}\n" for k, v in pairs.items()), encoding="utf-8"
|
|
)
|
|
from hermes_cli.config import invalidate_env_cache
|
|
|
|
invalidate_env_cache()
|
|
|
|
|
|
def _write_auth(home, pool):
|
|
home.joinpath("auth.json").write_text(
|
|
json.dumps({"credential_pool": pool}), encoding="utf-8"
|
|
)
|
|
|
|
|
|
def _read_auth(home):
|
|
return json.loads(home.joinpath("auth.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
def _zai_pool_fixture():
|
|
"""One env-seeded API-key entry plus one OAuth entry for the same provider."""
|
|
return {
|
|
"zai": [
|
|
{
|
|
"id": "e1",
|
|
"label": "env",
|
|
"auth_type": "api_key",
|
|
"priority": 0,
|
|
"source": "env:ZAI_API_KEY",
|
|
"access_token": FAKE_ZAI_KEY,
|
|
},
|
|
{
|
|
"id": "o1",
|
|
"label": "oauth",
|
|
"auth_type": "oauth",
|
|
"priority": 0,
|
|
"source": "device_code",
|
|
"access_token": FAKE_OAUTH_TOKEN,
|
|
"refresh_token": "rt-" + "d" * 16,
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DELETE — #51071 / #59761: stale credential_pool entries must be pruned
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def test_delete_clears_provider_models_cache(hermes_home):
|
|
_write_env(hermes_home, ZAI_API_KEY=FAKE_ZAI_KEY)
|
|
_write_auth(hermes_home, {"zai": [_zai_pool_fixture()["zai"][0]]})
|
|
cache_path = hermes_home / "provider_models_cache.json"
|
|
cache_path.write_text(
|
|
json.dumps({"zai": {"models": ["glm-5"], "ts": 0}}), encoding="utf-8"
|
|
)
|
|
|
|
resp = client.request(
|
|
"DELETE", "/api/env", json={"key": "ZAI_API_KEY"}, headers=HEADERS
|
|
)
|
|
assert resp.status_code == 200
|
|
if cache_path.exists():
|
|
cache = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
assert "zai" not in cache
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# UPDATE — #62269: config.yaml mirrors of the old key must rotate with .env
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _write_config(home, text):
|
|
home.joinpath("config.yaml").write_text(text, encoding="utf-8")
|
|
|
|
|
|
def test_update_rotates_config_yaml_model_mirror(hermes_home):
|
|
old = "sk-oe-" + "f" * 24
|
|
new = "sk-oe-" + "g" * 24
|
|
_write_env(hermes_home, OPENAI_API_KEY=old)
|
|
_write_config(
|
|
hermes_home,
|
|
"model:\n"
|
|
" provider: custom\n"
|
|
" default: my-model\n"
|
|
" base_url: https://llm.example.test/v1\n"
|
|
f" api_key: {old}\n",
|
|
)
|
|
|
|
resp = client.put(
|
|
"/api/env", json={"key": "OPENAI_API_KEY", "value": new}, headers=HEADERS
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "model.api_key" in resp.json().get("config_updates", [])
|
|
|
|
cfg_text = hermes_home.joinpath("config.yaml").read_text(encoding="utf-8")
|
|
assert old not in cfg_text, "stale old key left in config.yaml (#62269)"
|
|
assert new in cfg_text, "config.yaml mirror not rotated to the new key"
|
|
|
|
from hermes_cli.config import load_env
|
|
|
|
assert load_env()["OPENAI_API_KEY"] == new
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Suppression round-trip: delete sticks, re-add lifts it
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|