/ tests / hermes_cli / test_setup_noninteractive.py
test_setup_noninteractive.py
  1  """Tests for non-interactive setup and first-run headless behavior."""
  2  
  3  from argparse import Namespace
  4  from unittest.mock import MagicMock, patch
  5  
  6  import pytest
  7  from hermes_cli.config import DEFAULT_CONFIG, load_config, save_config
  8  
  9  
 10  def _make_setup_args(**overrides):
 11      return Namespace(
 12          non_interactive=overrides.get("non_interactive", False),
 13          section=overrides.get("section", None),
 14          reset=overrides.get("reset", False),
 15      )
 16  
 17  
 18  def _make_chat_args(**overrides):
 19      return Namespace(
 20          continue_last=overrides.get("continue_last", None),
 21          resume=overrides.get("resume", None),
 22          model=overrides.get("model", None),
 23          provider=overrides.get("provider", None),
 24          toolsets=overrides.get("toolsets", None),
 25          verbose=overrides.get("verbose", False),
 26          query=overrides.get("query", None),
 27          worktree=overrides.get("worktree", False),
 28          yolo=overrides.get("yolo", False),
 29          pass_session_id=overrides.get("pass_session_id", False),
 30          quiet=overrides.get("quiet", False),
 31          checkpoints=overrides.get("checkpoints", False),
 32      )
 33  
 34  
 35  class TestNonInteractiveSetup:
 36      """Verify setup paths exit cleanly in headless/non-interactive environments."""
 37  
 38      def test_cmd_setup_allows_noninteractive_flag_without_tty(self):
 39          """The CLI entrypoint should not block --non-interactive before setup.py handles it."""
 40          from hermes_cli.main import cmd_setup
 41  
 42          args = _make_setup_args(non_interactive=True)
 43  
 44          with (
 45              patch("hermes_cli.setup.run_setup_wizard") as mock_run_setup,
 46              patch("sys.stdin") as mock_stdin,
 47          ):
 48              mock_stdin.isatty.return_value = False
 49              cmd_setup(args)
 50  
 51          mock_run_setup.assert_called_once_with(args)
 52  
 53      def test_cmd_setup_defers_no_tty_handling_to_setup_wizard(self):
 54          """Bare `hermes setup` should reach setup.py, which prints headless guidance."""
 55          from hermes_cli.main import cmd_setup
 56  
 57          args = _make_setup_args(non_interactive=False)
 58  
 59          with (
 60              patch("hermes_cli.setup.run_setup_wizard") as mock_run_setup,
 61              patch("sys.stdin") as mock_stdin,
 62          ):
 63              mock_stdin.isatty.return_value = False
 64              cmd_setup(args)
 65  
 66          mock_run_setup.assert_called_once_with(args)
 67  
 68      def test_non_interactive_flag_skips_wizard(self, capsys):
 69          """--non-interactive should print guidance and not enter the wizard."""
 70          from hermes_cli.setup import run_setup_wizard
 71  
 72          args = _make_setup_args(non_interactive=True)
 73  
 74          with (
 75              patch("hermes_cli.setup.ensure_hermes_home"),
 76              patch("hermes_cli.setup.load_config", return_value={}),
 77              patch("hermes_cli.setup.get_hermes_home", return_value="/tmp/.hermes"),
 78              patch("hermes_cli.auth.get_active_provider", side_effect=AssertionError("wizard continued")),
 79              patch("builtins.input", side_effect=AssertionError("input should not be called")),
 80          ):
 81              run_setup_wizard(args)
 82  
 83          out = capsys.readouterr().out
 84          assert "hermes config set model.provider custom" in out
 85  
 86      def test_no_tty_skips_wizard(self, capsys):
 87          """When stdin has no TTY, the setup wizard should print guidance and return."""
 88          from hermes_cli.setup import run_setup_wizard
 89  
 90          args = _make_setup_args(non_interactive=False)
 91  
 92          with (
 93              patch("hermes_cli.setup.ensure_hermes_home"),
 94              patch("hermes_cli.setup.load_config", return_value={}),
 95              patch("hermes_cli.setup.get_hermes_home", return_value="/tmp/.hermes"),
 96              patch("hermes_cli.auth.get_active_provider", side_effect=AssertionError("wizard continued")),
 97              patch("sys.stdin") as mock_stdin,
 98              patch("builtins.input", side_effect=AssertionError("input should not be called")),
 99          ):
100              mock_stdin.isatty.return_value = False
101              run_setup_wizard(args)
102  
103          out = capsys.readouterr().out
104          assert "hermes config set model.provider custom" in out
105  
106      def test_reset_flag_rewrites_config_before_noninteractive_exit(self, tmp_path, monkeypatch, capsys):
107          """--reset should rewrite config.yaml even when the wizard cannot run interactively."""
108          from hermes_cli.setup import run_setup_wizard
109  
110          monkeypatch.setenv("HERMES_HOME", str(tmp_path))
111          cfg = load_config()
112          cfg["model"] = {"provider": "custom", "base_url": "http://localhost:8080/v1", "default": "llama3"}
113          cfg["agent"]["max_turns"] = 12
114          save_config(cfg)
115  
116          args = _make_setup_args(non_interactive=True, reset=True)
117  
118          run_setup_wizard(args)
119  
120          reloaded = load_config()
121          assert reloaded["model"] == DEFAULT_CONFIG["model"]
122          assert reloaded["agent"]["max_turns"] == DEFAULT_CONFIG["agent"]["max_turns"]
123          out = capsys.readouterr().out
124          assert "Configuration reset to defaults." in out
125  
126      def test_chat_first_run_headless_skips_setup_prompt(self, capsys):
127          """Bare `hermes` should not prompt for input when no provider exists and stdin is headless."""
128          from hermes_cli.main import cmd_chat
129  
130          args = _make_chat_args()
131  
132          with (
133              patch("hermes_cli.main._has_any_provider_configured", return_value=False),
134              patch("hermes_cli.main.cmd_setup") as mock_setup,
135              patch("sys.stdin") as mock_stdin,
136              patch("builtins.input", side_effect=AssertionError("input should not be called")),
137          ):
138              mock_stdin.isatty.return_value = False
139              with pytest.raises(SystemExit) as exc:
140                  cmd_chat(args)
141  
142          assert exc.value.code == 1
143          mock_setup.assert_not_called()
144          out = capsys.readouterr().out
145          assert "hermes config set model.provider custom" in out
146  
147      def test_main_accepts_tts_setup_section(self, monkeypatch):
148          """`hermes setup tts` should parse and dispatch like other setup sections."""
149          from hermes_cli import main as main_mod
150  
151          received = {}
152  
153          def fake_cmd_setup(args):
154              received["section"] = args.section
155  
156          monkeypatch.setattr(main_mod, "cmd_setup", fake_cmd_setup)
157          monkeypatch.setattr("sys.argv", ["hermes", "setup", "tts"])
158  
159          main_mod.main()
160  
161          assert received["section"] == "tts"