Files
hermes-agent/tests/plugins/platforms/test_interactive_setup_reconfigure_gate.py
teknium1 4c4ad0d387 feat(discord): setup checks the bot token with Discord and prints the invite link
Discord was the hardest gateway to set up: the .env template had no Discord
section at all, the wizard saved whatever was pasted, and users had to find
the Message Content Intent toggle, build an OAuth2 invite URL by hand and turn
on Developer Mode just to learn their own user ID.

The wizard now reads GET /applications/@me with the pasted token: a rejected
token is re-prompted instead of saved, an intent that is off gets a direct
link to the toggle and a re-check, the invite link is printed with the
permissions the adapter uses, and the allowlist defaults to the application's
owner (or accepted team members). Offline, the token is still saved with the
manual intent reminder. The wizard moves from the 7k-line adapter into
plugins/platforms/discord/onboarding.py.
2026-10-03 23:40:33 -07:00

72 lines
3.4 KiB
Python

"""Every platform plugin's ``interactive_setup`` routes its "already configured? Reconfigure?"
gate through ``hermes_cli.setup_platforms.declines_reconfigure``.
The wizards used to hand-roll the gate (env check + info line + ``prompt_yes_no(..., False)``)
with drifting wording and, for two of them, raw ``input()`` loops. Fixes to the shared gate
(wording, default, non-interactive handling) only reach a wizard that actually calls it, so the
test spies on the real primitive: with the primary env var set and the user answering "no", the
wizard must have asked the shared gate about that env var and returned without persisting or
prompting anything else.
"""
import importlib
import pytest
import hermes_cli.setup_platforms as setup_platforms_mod
# (plugin module, primary env var the shared gate keys on)
_WIZARDS = [
("plugins.platforms.buzz.adapter", "BUZZ_RELAY_URL"),
("plugins.platforms.dingtalk.adapter", "DINGTALK_CLIENT_ID"),
("plugins.platforms.discord.onboarding", "DISCORD_BOT_TOKEN"),
("plugins.platforms.feishu.adapter", "FEISHU_APP_ID"),
("plugins.platforms.google_chat.adapter", "GOOGLE_CHAT_SUBSCRIPTION_NAME"),
("plugins.platforms.irc.adapter", "IRC_SERVER"),
("plugins.platforms.line.adapter", "LINE_CHANNEL_ACCESS_TOKEN"),
("plugins.platforms.matrix.adapter", "MATRIX_PASSWORD"),
("plugins.platforms.mattermost.adapter", "MATTERMOST_TOKEN"),
("plugins.platforms.raft.adapter", "RAFT_PROFILE"),
("plugins.platforms.simplex.adapter", "SIMPLEX_WS_URL"),
("plugins.platforms.slack.adapter", "SLACK_BOT_TOKEN"),
("plugins.platforms.teams.adapter", "TEAMS_CLIENT_ID"),
("plugins.platforms.wecom.adapter", "WECOM_BOT_ID"),
]
@pytest.mark.parametrize("module_name, env_var", _WIZARDS, ids=[m.split(".")[-2] for m, _ in _WIZARDS])
def test_declining_reconfigure_goes_through_shared_gate(monkeypatch, tmp_path, module_name, env_var):
import hermes_cli.cli_output as cli_output_mod
import hermes_cli.config as config_mod
import hermes_cli.setup as setup_mod
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv(env_var, "already-set")
gated: list[tuple[str, ...]] = []
real_gate = setup_platforms_mod.declines_reconfigure
def _spy_gate(label, question, *env_vars):
gated.append(env_vars)
return real_gate(label, question, *env_vars)
def _no_save(*_a, **_kw):
raise AssertionError("wizard persisted env after the user declined to reconfigure")
def _no_prompt(*_a, **_kw):
raise AssertionError("wizard fell through to its own prompts after the user declined")
# Wizards import the gate lazily from setup_platforms; the gate reads prompt_yes_no through
# hermes_cli.setup. Everything else a wizard could do after declining must not happen.
monkeypatch.setattr(setup_platforms_mod, "declines_reconfigure", _spy_gate)
monkeypatch.setattr(setup_mod, "prompt_yes_no", lambda *_a, **_kw: False)
monkeypatch.setattr(cli_output_mod, "prompt_yes_no", lambda *_a, **_kw: False)
for mod in (setup_mod, cli_output_mod):
monkeypatch.setattr(mod, "prompt", _no_prompt)
monkeypatch.setattr(mod, "save_env_value", _no_save, raising=False)
monkeypatch.setattr(config_mod, "save_env_value", _no_save)
importlib.import_module(module_name).interactive_setup()
assert gated and env_var in gated[0], f"{module_name}: gate not routed through declines_reconfigure: {gated}"