hermes-agent/tests/gateway/relay/test_self_provision.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
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).
2026-07-29 13:10:23 -07:00

259 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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