"""Tests for the configurable default interface (cli vs tui). `hermes` launches the classic prompt_toolkit REPL by default, but users can flip ``display.interface: tui`` in config.yaml to make the modern Ink TUI the default for bare ``hermes`` / ``hermes chat``. Explicit flags always win: --cli forces the classic REPL (highest precedence) --tui / HERMES_TUI=1 forces the TUI display.interface the configured default (unset) classic REPL These tests pin that precedence at every layer that makes the decision: * ``_resolve_use_tui(args)`` — the canonical args-aware resolver used by ``cmd_chat`` and the Termux fast-TUI path. * ``_wants_tui_early(argv)`` — the dependency-free early resolver used by mouse-residue suppression and the Termux fast paths, before argparse and ``hermes_cli.config`` are importable. * the argument parser — both ``--cli`` and ``--tui`` parse at the top level and under the ``chat`` subcommand and are relaunch-inherited. """ from __future__ import annotations import os from types import SimpleNamespace import pytest from hermes_cli import main as m @pytest.fixture(autouse=True) def _reset_early_cache(monkeypatch): # The early resolver memoizes the config read; clear it so each test sees # a fresh value, and make sure no stray HERMES_TUI leaks in. monkeypatch.setattr(m, "_EARLY_INTERFACE_CACHE", None) monkeypatch.delenv("HERMES_TUI", raising=False) yield monkeypatch.setattr(m, "_EARLY_INTERFACE_CACHE", None) def _args(**kw): kw.setdefault("cli", False) kw.setdefault("tui", False) return SimpleNamespace(**kw) def _patch_config(monkeypatch, interface): import hermes_cli.config as cfg monkeypatch.setattr( cfg, "load_config", lambda: {"display": {"interface": interface}} ) # --------------------------------------------------------------------------- # _resolve_use_tui — args-aware resolver # --------------------------------------------------------------------------- class TestResolveUseTui: def test_cli_flag_beats_config_tui(self, monkeypatch): _patch_config(monkeypatch, "tui") assert m._resolve_use_tui(_args(cli=True)) is False def test_cli_flag_beats_tui_flag_and_env(self, monkeypatch): _patch_config(monkeypatch, "tui") monkeypatch.setenv("HERMES_TUI", "1") assert m._resolve_use_tui(_args(cli=True, tui=True)) is False def test_tui_flag_beats_config_cli(self, monkeypatch): _patch_config(monkeypatch, "cli") assert m._resolve_use_tui(_args(tui=True)) is True def test_env_beats_config_cli(self, monkeypatch): _patch_config(monkeypatch, "cli") monkeypatch.setenv("HERMES_TUI", "1") assert m._resolve_use_tui(_args()) is True def test_config_tui_with_no_flags(self, monkeypatch): _patch_config(monkeypatch, "tui") assert m._resolve_use_tui(_args()) is True def test_config_cli_is_default(self, monkeypatch): _patch_config(monkeypatch, "cli") assert m._resolve_use_tui(_args()) is False def test_interface_value_is_case_insensitive(self, monkeypatch): _patch_config(monkeypatch, "TUI") assert m._resolve_use_tui(_args()) is True def test_load_config_failure_falls_back_to_cli(self, monkeypatch): import hermes_cli.config as cfg def boom(): raise RuntimeError("config unreadable") monkeypatch.setattr(cfg, "load_config", boom) assert m._resolve_use_tui(_args()) is False # --------------------------------------------------------------------------- # _wants_tui_early — dependency-free early resolver # --------------------------------------------------------------------------- class TestWantsTuiEarly: @pytest.fixture def home_with_interface(self, tmp_path, monkeypatch): def _make(interface): (tmp_path / "config.yaml").write_text( f"display:\n interface: {interface}\n" ) monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setattr(m, "_EARLY_INTERFACE_CACHE", None) return _make def test_config_tui_bare_argv(self, home_with_interface): home_with_interface("tui") assert m._wants_tui_early([]) is True def test_cli_flag_overrides_config_tui(self, home_with_interface): home_with_interface("tui") assert m._wants_tui_early(["--cli"]) is False def test_tui_flag_with_config_cli(self, home_with_interface): home_with_interface("cli") assert m._wants_tui_early(["--tui"]) is True def test_env_with_config_cli(self, home_with_interface, monkeypatch): home_with_interface("cli") monkeypatch.setenv("HERMES_TUI", "1") assert m._wants_tui_early([]) is True def test_config_cli_bare_argv(self, home_with_interface): home_with_interface("cli") assert m._wants_tui_early([]) is False def test_missing_config_defaults_to_cli(self, tmp_path, monkeypatch): # HERMES_HOME points at an empty dir — no config.yaml. monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setattr(m, "_EARLY_INTERFACE_CACHE", None) assert m._wants_tui_early([]) is False def test_unreadable_config_defaults_to_cli(self, tmp_path, monkeypatch): # Garbage YAML must not crash the hot path; falls back to cli. (tmp_path / "config.yaml").write_text("this: : : not valid yaml\n") monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setattr(m, "_EARLY_INTERFACE_CACHE", None) assert m._wants_tui_early([]) is False # --------------------------------------------------------------------------- # argument parser — flags exist at both levels and are relaunch-inherited # --------------------------------------------------------------------------- class TestParserFlags: def _parser(self): from hermes_cli._parser import build_top_level_parser parser, _subparsers, _chat = build_top_level_parser() return parser def test_top_level_cli_flag(self): args = self._parser().parse_args(["--cli"]) assert args.cli is True and args.tui is False def test_top_level_tui_flag(self): args = self._parser().parse_args(["--tui"]) assert args.tui is True and args.cli is False def test_chat_subcommand_cli_flag(self): args = self._parser().parse_args(["chat", "--cli"]) assert args.cli is True def test_chat_subcommand_tui_flag(self): args = self._parser().parse_args(["chat", "--tui"]) assert args.tui is True def test_cli_and_tui_are_relaunch_inherited(self): from hermes_cli.relaunch import _INHERITED_FLAGS_TABLE inherited = {flag for flag, _takes_value in _INHERITED_FLAGS_TABLE} assert "--cli" in inherited assert "--tui" in inherited # --------------------------------------------------------------------------- # config default — shipped default preserves classic behavior # --------------------------------------------------------------------------- def test_default_config_interface_is_cli(): from hermes_cli.config import DEFAULT_CONFIG assert DEFAULT_CONFIG["display"]["interface"] == "cli"