"""Tests for ``hermes gui`` desktop launcher wiring.""" from __future__ import annotations import argparse import subprocess import sys from pathlib import Path from unittest.mock import patch import pytest from hermes_cli import main as cli_main def _ns(**kw): defaults = dict( skip_build=False, fake_boot=False, ignore_existing=False, hermes_root=None, cwd=None, ) defaults.update(kw) return argparse.Namespace(**defaults) def _make_desktop_tree(tmp_path: Path) -> Path: root = tmp_path / "hermes-agent" desktop_dir = root / "apps" / "desktop" desktop_dir.mkdir(parents=True) (desktop_dir / "package.json").write_text("{}", encoding="utf-8") return root def test_gui_installs_builds_and_launches_desktop(tmp_path, monkeypatch): root = _make_desktop_tree(tmp_path) desktop_dir = root / "apps" / "desktop" monkeypatch.setattr(cli_main, "PROJECT_ROOT", root) install_ok = subprocess.CompletedProcess(["npm", "ci"], 0) build_ok = subprocess.CompletedProcess(["npm", "run", "build"], 0) launch_ok = subprocess.CompletedProcess(["npm", "exec", "--", "electron", "."], 0) with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \ patch("hermes_cli.main._run_npm_install_deterministic", return_value=install_ok) as mock_install, \ patch("hermes_cli.main.subprocess.run", side_effect=[build_ok, launch_ok]) as mock_run, \ pytest.raises(SystemExit) as exc: cli_main.cmd_gui(_ns()) assert exc.value.code == 0 mock_install.assert_called_once_with("/usr/bin/npm", root, capture_output=False) assert mock_run.call_args_list[0].args[0] == ["/usr/bin/npm", "run", "build"] assert mock_run.call_args_list[0].kwargs["cwd"] == desktop_dir assert mock_run.call_args_list[1].args[0] == ["/usr/bin/npm", "exec", "--", "electron", "."] assert mock_run.call_args_list[1].kwargs["cwd"] == desktop_dir def test_gui_forwards_desktop_environment_overrides(tmp_path, monkeypatch): root = _make_desktop_tree(tmp_path) hermes_root = tmp_path / "custom-hermes" cwd = tmp_path / "project" hermes_root.mkdir() cwd.mkdir() monkeypatch.setattr(cli_main, "PROJECT_ROOT", root) ok = subprocess.CompletedProcess([], 0) with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \ patch("hermes_cli.main._run_npm_install_deterministic", return_value=ok), \ patch("hermes_cli.main.subprocess.run", side_effect=[ok, ok]) as mock_run, \ pytest.raises(SystemExit): cli_main.cmd_gui(_ns( fake_boot=True, ignore_existing=True, hermes_root=str(hermes_root), cwd=str(cwd), )) launch_env = mock_run.call_args_list[1].kwargs["env"] assert launch_env["HERMES_DESKTOP_BOOT_FAKE"] == "1" assert launch_env["HERMES_DESKTOP_IGNORE_EXISTING"] == "1" assert launch_env["HERMES_DESKTOP_HERMES_ROOT"] == str(hermes_root) assert launch_env["HERMES_DESKTOP_CWD"] == str(cwd) def test_gui_exits_when_npm_missing(tmp_path, monkeypatch, capsys): root = _make_desktop_tree(tmp_path) monkeypatch.setattr(cli_main, "PROJECT_ROOT", root) with patch("hermes_cli.main.shutil.which", return_value=None), \ pytest.raises(SystemExit) as exc: cli_main.cmd_gui(_ns()) assert exc.value.code == 1 assert "npm was not found" in capsys.readouterr().out def test_gui_skip_build_requires_existing_dist(tmp_path, monkeypatch, capsys): root = _make_desktop_tree(tmp_path) monkeypatch.setattr(cli_main, "PROJECT_ROOT", root) with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \ pytest.raises(SystemExit) as exc: cli_main.cmd_gui(_ns(skip_build=True)) assert exc.value.code == 1 assert "no desktop dist found" in capsys.readouterr().out def test_gui_skip_build_launches_existing_dist_without_install_or_build(tmp_path, monkeypatch): root = _make_desktop_tree(tmp_path) desktop_dir = root / "apps" / "desktop" (desktop_dir / "dist").mkdir() (desktop_dir / "dist" / "index.html").write_text("
", encoding="utf-8") electron_pkg = root / "node_modules" / "electron" electron_pkg.mkdir(parents=True) (electron_pkg / "package.json").write_text("{}", encoding="utf-8") monkeypatch.setattr(cli_main, "PROJECT_ROOT", root) launch_ok = subprocess.CompletedProcess(["npm", "exec", "--", "electron", "."], 0) with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \ patch("hermes_cli.main._run_npm_install_deterministic") as mock_install, \ patch("hermes_cli.main.subprocess.run", return_value=launch_ok) as mock_run, \ pytest.raises(SystemExit) as exc: cli_main.cmd_gui(_ns(skip_build=True)) assert exc.value.code == 0 mock_install.assert_not_called() mock_run.assert_called_once() assert mock_run.call_args.args[0] == ["/usr/bin/npm", "exec", "--", "electron", "."] @pytest.mark.parametrize( "argv", [ ["hermes", "gui"], ["hermes", "-m", "gpt5", "gui"], ], ) def test_gui_is_known_builtin_for_plugin_gating(argv): with patch.object(sys, "argv", argv): assert cli_main._plugin_cli_discovery_needed() is False