Files
hermes-agent/tests/plugins/platforms/test_interactive_setup_reconfigure_gate.py
teknium1 e73f94fa83 refactor(platforms): every plugin setup wizard uses the shared declines_reconfigure gate
Fourteen platform plugins hand-rolled the "already configured? Reconfigure? [y/N]"
gate at the top of interactive_setup (env check + info line + prompt_yes_no(..., False)),
with drifting wording ("X: already configured" vs "X is already configured." vs
"already enabled") and, for LINE and SimpleX, raw input() loops with their own
EOF/KeyboardInterrupt handling and no gate at all. Fixes to the gate (non-interactive
handling, wording, default) therefore reached only the core Telegram/BlueBubbles/webhook
wizards.

- hermes_cli/setup_platforms.py: `_declines_reconfigure` becomes the public
  `declines_reconfigure(label, question, *env_vars)` (any-of env check, so Matrix's
  token-or-password gate fits); `_save_prompted` becomes `save_prompted` alongside it.
  No alias kept; the three core callers are updated.
- buzz, dingtalk, discord, feishu, google_chat, irc, matrix, mattermost, raft, slack,
  teams, wecom: the hand-rolled gate is replaced by one `declines_reconfigure(...)` call;
  post-decline extras (Discord allowlist nudge, Slack manifest refresh, Raft "Keeping"
  line) stay local and unchanged.
- line, simplex: the raw input() loops move onto hermes_cli.cli_output.prompt (masked
  for secrets, "" on Ctrl-C/EOF) and gain the shared gate on their primary env var.

Behavior change: the gate's info line is now uniformly "<Label>: already configured"
(DingTalk/Feishu/WeCom lose the trailing period + inline ID; Buzz/IRC/Google Chat/Raft/
Teams no longer echo the current value in that line). Feishu and WeCom now gate on the
app/bot ID alone instead of ID AND secret. LINE and SimpleX gain a "Reconfigure?" [y/N]
prompt when already configured; their prompts now honour HERMES_NONINTERACTIVE and print
via the CLI helpers instead of bare print(). Prompt defaults (No) are unchanged everywhere.

Not touched: WhatsApp's gate keys on WHATSAPP_ENABLED being truthy (a "false" value must
not count as configured), which the shared any-set gate cannot express — left hand-rolled.

Test: tests/plugins/platforms/test_interactive_setup_reconfigure_gate.py parametrized over
the 14 wizards — with the primary env var set and the user declining, each wizard must have
called declines_reconfigure with that var and returned without prompting or saving.
Sabotage: reverting mattermost's gate fails that row.
2026-09-13 05:32:38 -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.adapter", "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}"