mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Systematic prune per AGENTS.md test policy, one pass over every major test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli, cron, tui_gateway, honcho/openviking, root-level): - DELETE: source-reading tests (read_text/getsource on prod files), change-detector tests (exact catalog counts, model-name snapshots, config version literals), mock-echo tests (assert a mock returns what it was told), assertion-free/trivial tests, near-duplicate parametrizations (boundaries + one representative kept), async/sync twin duplicates, cosmetic within-file variations. - KEEP (mandatory): security/redaction/approval guards, message-role alternation invariants, prompt-caching/deterministic-call-id invariants, issue-number regression tests (deduped), E2E tests. - 6 test files deleted outright (script-style/no-assert or fully redundant); conftest.py, fakes/, fixtures/ untouched. - tests/acp/conftest.py added: autouse fixture stubs the live models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server tests performed on every session create — test_server.py 147s → 3.4s, and the tests are now genuinely hermetic. - Sleep-based slowness shrunk where safe (codex_ttfb_watchdog, compression_concurrent_fork, etc.); no wall-clock assertion tightened. Verification: full hermetic suite via scripts/run_tests.sh — 2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall (baseline: 583s wall, 13,564s subprocess CPU).
259 lines
10 KiB
Python
259 lines
10 KiB
Python
"""Unit tests for boot-time relay self-provisioning.
|
||
|
||
Covers gateway.relay.self_provision_relay() + the relay_endpoint() /
|
||
relay_route_keys() config readers. The connector HTTP POST is monkeypatched
|
||
(the cross-repo E2E exercises the real /relay/provision); these prove the
|
||
TRIGGER logic, in-process env wiring, and fail-soft boot behaviour.
|
||
|
||
The trigger is deliberately NOT is_managed() (that means NixOS/package-manager-
|
||
managed, which is False on a NAS-hosted Fly agent). The real gate is
|
||
"relay_url set + no pinned secret + a resolvable NAS token".
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
import pytest
|
||
|
||
import gateway.relay as relay
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _clean_env(monkeypatch):
|
||
for k in (
|
||
"GATEWAY_RELAY_URL",
|
||
"GATEWAY_RELAY_ID",
|
||
"GATEWAY_RELAY_SECRET",
|
||
"GATEWAY_RELAY_DELIVERY_KEY",
|
||
"GATEWAY_RELAY_ENDPOINT",
|
||
"GATEWAY_RELAY_ROUTE_KEYS",
|
||
"GATEWAY_RELAY_PLATFORM",
|
||
"GATEWAY_RELAY_BOT_ID",
|
||
"GATEWAY_RELAY_INSTANCE_ID",
|
||
"GATEWAY_RELAY_WAKE_URL",
|
||
):
|
||
monkeypatch.delenv(k, raising=False)
|
||
# Never read config.yaml off disk in these tests.
|
||
monkeypatch.setattr("gateway.run._load_gateway_config", lambda: {}, raising=False)
|
||
|
||
|
||
def _stub_post(captured: dict):
|
||
"""A fake _post_provision that records its kwargs and returns creds."""
|
||
|
||
def _fake(**kwargs):
|
||
captured.update(kwargs)
|
||
return {
|
||
"secret": "a" * 64,
|
||
"deliveryKey": "b" * 64,
|
||
"tenant": "org-tenant-x",
|
||
"gatewayId": kwargs["gateway_id"],
|
||
"routeKeys": kwargs["route_keys"],
|
||
}
|
||
|
||
return _fake
|
||
|
||
|
||
def _arm(monkeypatch, *, url="wss://connector.example/relay", token="nas-token"):
|
||
"""Arm the real trigger: a relay URL + a resolvable NAS token.
|
||
|
||
Note there is intentionally no `managed` knob — self-provision no longer
|
||
consults is_managed(). A test that wants the "no NAS identity" branch
|
||
monkeypatches resolve_nous_access_token to raise instead.
|
||
"""
|
||
monkeypatch.setattr(relay, "relay_url", lambda: url)
|
||
monkeypatch.setattr("hermes_cli.auth.resolve_nous_access_token", lambda: token)
|
||
|
||
|
||
# ─────────────────────────── config readers ───────────────────────────
|
||
|
||
|
||
def test_provision_url_maps_ws_to_http():
|
||
assert relay._provision_url("wss://c.example/relay") == "https://c.example/relay/provision"
|
||
assert relay._provision_url("ws://c.example/relay") == "http://c.example/relay/provision"
|
||
assert relay._provision_url("https://c.example") == "https://c.example/relay/provision"
|
||
|
||
|
||
# ─────────────────────────── trigger logic ───────────────────────────
|
||
|
||
|
||
def test_skips_when_relay_not_configured(monkeypatch):
|
||
_arm(monkeypatch, url=None)
|
||
called = {"n": 0}
|
||
monkeypatch.setattr(relay, "_post_provision", lambda **k: called.__setitem__("n", called["n"] + 1) or {})
|
||
assert relay.self_provision_relay() is False
|
||
assert called["n"] == 0
|
||
|
||
|
||
def test_skips_when_secret_already_pinned(monkeypatch):
|
||
"""A self-hosted, enrolled gateway has a pinned secret -> never self-provisions."""
|
||
_arm(monkeypatch)
|
||
monkeypatch.setenv("GATEWAY_RELAY_ID", "gw-pinned")
|
||
monkeypatch.setenv("GATEWAY_RELAY_SECRET", "deadbeef")
|
||
called = {"n": 0}
|
||
monkeypatch.setattr(relay, "_post_provision", lambda **k: called.__setitem__("n", called["n"] + 1) or {})
|
||
assert relay.self_provision_relay() is False
|
||
assert called["n"] == 0
|
||
# The pinned secret is untouched.
|
||
assert relay.relay_connection_auth() == ("gw-pinned", "deadbeef")
|
||
|
||
|
||
# ─────────────────────────── happy path ───────────────────────────
|
||
|
||
def test_provisions_and_sets_env_in_process(monkeypatch):
|
||
_arm(monkeypatch)
|
||
monkeypatch.setenv("GATEWAY_RELAY_ENDPOINT", "https://gw.example.com/inbound")
|
||
monkeypatch.setenv("GATEWAY_RELAY_ROUTE_KEYS", "guild-1,guild-2")
|
||
captured: dict = {}
|
||
monkeypatch.setattr(relay, "_post_provision", _stub_post(captured))
|
||
|
||
assert relay.self_provision_relay() is True
|
||
# The connector POST carried the gateway-asserted endpoint + route keys.
|
||
assert captured["provision_url"] == "https://connector.example/relay/provision"
|
||
assert captured["access_token"] == "nas-token"
|
||
assert captured["gateway_endpoint"] == "https://gw.example.com/inbound"
|
||
assert captured["route_keys"] == ["guild-1", "guild-2"]
|
||
# Creds landed in os.environ (in-process), so register_relay_adapter() reads them.
|
||
gid, secret = relay.relay_connection_auth()
|
||
assert gid and secret == "a" * 64
|
||
# The delivery key is persisted in-process too (issued by the connector,
|
||
# kept for forward-compat; inbound rides the WS so it isn't consumed).
|
||
assert os.environ["GATEWAY_RELAY_DELIVERY_KEY"] == "b" * 64
|
||
|
||
|
||
def test_outbound_only_when_no_endpoint(monkeypatch):
|
||
_arm(monkeypatch)
|
||
captured: dict = {}
|
||
monkeypatch.setattr(relay, "_post_provision", _stub_post(captured))
|
||
|
||
assert relay.self_provision_relay() is True
|
||
assert captured["gateway_endpoint"] is None
|
||
assert captured["route_keys"] == []
|
||
assert relay.relay_connection_auth()[1] == "a" * 64
|
||
|
||
|
||
# ─────────────────── instance-id forwarding (Phase 6 Unit α) ───────────────────
|
||
|
||
|
||
def test_instance_id_absent_forwards_none(monkeypatch):
|
||
"""No stamp (self-hosted / pre-Phase-6) -> instance_id None; the connector
|
||
stores null and per-instance routing simply has no binding yet."""
|
||
_arm(monkeypatch)
|
||
captured: dict = {}
|
||
monkeypatch.setattr(relay, "_post_provision", _stub_post(captured))
|
||
|
||
assert relay.self_provision_relay() is True
|
||
assert captured["instance_id"] is None
|
||
|
||
|
||
def test_post_provision_body_includes_instanceId_only_when_set(monkeypatch):
|
||
"""The real _post_provision adds `instanceId` to the JSON body ONLY when a
|
||
value is supplied — omitting it lets the connector store null (back-compat),
|
||
rather than binding an empty string."""
|
||
import json
|
||
|
||
sent: dict = {}
|
||
|
||
class _Resp:
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *a):
|
||
return False
|
||
|
||
def read(self):
|
||
return json.dumps({"secret": "a" * 64, "deliveryKey": "b" * 64, "tenant": "t", "gatewayId": "gw-1"}).encode()
|
||
|
||
def _fake_urlopen(req, timeout=None): # noqa: ANN001
|
||
sent["body"] = json.loads(req.data.decode())
|
||
return _Resp()
|
||
|
||
monkeypatch.setattr("urllib.request.urlopen", _fake_urlopen)
|
||
|
||
# With an instance id -> present in the body.
|
||
relay._post_provision(
|
||
provision_url="https://c.example/relay/provision",
|
||
access_token="tok",
|
||
gateway_id="gw-1",
|
||
platform="discord",
|
||
bot_id="app",
|
||
gateway_endpoint=None,
|
||
route_keys=[],
|
||
instance_id="inst-abc",
|
||
)
|
||
assert sent["body"]["instanceId"] == "inst-abc"
|
||
|
||
# Without one -> the key is absent entirely (not "" ).
|
||
relay._post_provision(
|
||
provision_url="https://c.example/relay/provision",
|
||
access_token="tok",
|
||
gateway_id="gw-1",
|
||
platform="discord",
|
||
bot_id="app",
|
||
gateway_endpoint=None,
|
||
route_keys=[],
|
||
)
|
||
assert "instanceId" not in sent["body"]
|
||
|
||
|
||
# ─────────────────── wake-url forwarding (Phase 5 Unit C) ───────────────────
|
||
|
||
|
||
def test_forwards_wake_url_to_provision(monkeypatch):
|
||
"""A suspendable agent stamped with GATEWAY_RELAY_WAKE_URL forwards it to the
|
||
connector so the connector can poke it awake when the first buffered event
|
||
lands on a flipped destination (Unit C wake primitive)."""
|
||
_arm(monkeypatch)
|
||
monkeypatch.setenv("GATEWAY_RELAY_WAKE_URL", "https://wake.example/poke")
|
||
captured: dict = {}
|
||
monkeypatch.setattr(relay, "_post_provision", _stub_post(captured))
|
||
|
||
assert relay.self_provision_relay() is True
|
||
assert captured["wake_url"] == "https://wake.example/poke"
|
||
|
||
|
||
def test_wake_url_absent_forwards_none(monkeypatch):
|
||
"""No stamp (self-hosted / non-suspendable) -> wake_url None; the connector
|
||
stores null and simply never pokes (it can't wake what it can't reach)."""
|
||
_arm(monkeypatch)
|
||
captured: dict = {}
|
||
monkeypatch.setattr(relay, "_post_provision", _stub_post(captured))
|
||
|
||
assert relay.self_provision_relay() is True
|
||
assert captured["wake_url"] is None
|
||
|
||
|
||
# ─────────────────────────── fail-soft ───────────────────────────
|
||
|
||
def test_no_nas_token_is_non_fatal(monkeypatch):
|
||
"""A self-hosted box with a relay URL but no resolvable NAS identity skips
|
||
quietly (this is the branch that replaces the old is_managed() gate for the
|
||
non-NAS case)."""
|
||
monkeypatch.setattr(relay, "relay_url", lambda: "wss://connector.example/relay")
|
||
|
||
def _boom():
|
||
raise RuntimeError("no token")
|
||
|
||
monkeypatch.setattr("hermes_cli.auth.resolve_nous_access_token", _boom)
|
||
# Must not raise; returns False; no creds set.
|
||
assert relay.self_provision_relay() is False
|
||
assert relay.relay_connection_auth() == (None, None)
|
||
|
||
|
||
# ─────────────────────────── displayName (Phase 1 parity, gg#171) ───────────────────────────
|
||
|
||
|
||
def test_relay_display_name_suppresses_stock_brand(monkeypatch):
|
||
"""The default 'Hermes Agent' brand is identical on every install — forwarding
|
||
it would shadow the connector's linked-owner fallback (which actually
|
||
disambiguates) with a uniform label. Only customized names are forwarded."""
|
||
monkeypatch.delenv("GATEWAY_RELAY_DISPLAY_NAME", raising=False)
|
||
|
||
class _Skin:
|
||
def get_branding(self, key, fallback=""):
|
||
return "Hermes Agent" if key == "agent_name" else fallback
|
||
|
||
monkeypatch.setattr("hermes_cli.skin_engine.get_active_skin", lambda: _Skin())
|
||
assert relay.relay_display_name() is None
|
||
|
||
|