test(desktop): pin the detached Windows launch and clean Ctrl-C exit

Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
This commit is contained in:
Hermes Agent
2026-09-25 11:39:42 -05:00
committed by brooklyn!
parent 3ddf82fe24
commit 701a7559d3

View File

@@ -1015,6 +1015,138 @@ def test_gui_password_store_bridge_is_linux_only(tmp_path, monkeypatch):
assert "HERMES_DESKTOP_PASSWORD_STORE" not in launch_env
# ---------------------------------------------------------------------------
# #58275: the Windows packaged launch must detach from the parent console
# (Popen + windows_detach_flags, DEVNULL stdio, immediate exit 0) instead of
# a console-inheriting subprocess.run that dies with the launching shell.
# #59848: on the foreground platforms, Ctrl-C in the attached terminal must
# exit cleanly instead of raising a KeyboardInterrupt traceback.
# ---------------------------------------------------------------------------
@pytest.mark.platforms("windows")
def test_gui_win32_launches_detached_and_returns(tmp_path, monkeypatch):
import hermes_cli._subprocess_compat as _subproc_compat
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
packaged_exe = _make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
with patch("hermes_cli.source_build.prepare_source_dependencies", return_value=ok), \
patch("hermes_cli.main_desktop._desktop_build_needed", return_value=False), \
patch("hermes_cli.main_desktop._desktop_exe_integrity_error", return_value=None), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.main.subprocess.Popen") as mock_popen, \
patch("hermes_cli.main.subprocess.run") as mock_run, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns(skip_build=True))
# Parent returns cleanly so the user can close the launching shell.
assert exc.value.code == 0
# The blocking, console-inheriting run() path must NOT be used for launch.
mock_run.assert_not_called()
# Detached spawn happened exactly once, targeting the packaged exe with the
# Windows detach creationflags and fully severed stdio.
mock_popen.assert_called_once()
call = mock_popen.call_args
assert call.args[0][0] == str(packaged_exe)
assert call.kwargs["creationflags"] == _subproc_compat.windows_detach_flags()
assert call.kwargs["stdin"] is subprocess.DEVNULL
assert call.kwargs["stdout"] is subprocess.DEVNULL
assert call.kwargs["stderr"] is subprocess.DEVNULL
@pytest.mark.platforms("windows")
def test_gui_win32_detach_falls_back_without_breakaway(tmp_path, monkeypatch):
import hermes_cli._subprocess_compat as _subproc_compat
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
breakaway_denied = PermissionError("breakaway denied")
breakaway_denied.winerror = 5
with patch("hermes_cli.source_build.prepare_source_dependencies", return_value=ok), \
patch("hermes_cli.main_desktop._desktop_build_needed", return_value=False), \
patch("hermes_cli.main_desktop._desktop_exe_integrity_error", return_value=None), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.main.subprocess.Popen",
side_effect=[breakaway_denied, None]) as mock_popen, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns(skip_build=True))
assert exc.value.code == 0
assert mock_popen.call_count == 2
assert (
mock_popen.call_args_list[0].kwargs["creationflags"]
== _subproc_compat.windows_detach_flags()
)
assert (
mock_popen.call_args_list[1].kwargs["creationflags"]
== _subproc_compat.windows_detach_flags_without_breakaway()
)
@pytest.mark.platforms("windows")
def test_gui_win32_detach_reraises_non_breakaway_oserror(tmp_path, monkeypatch):
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
spawn_error = OSError("The system cannot find the file specified")
spawn_error.winerror = 2 # ERROR_FILE_NOT_FOUND — unrelated to breakaway.
with patch("hermes_cli.source_build.prepare_source_dependencies", return_value=ok), \
patch("hermes_cli.main_desktop._desktop_build_needed", return_value=False), \
patch("hermes_cli.main_desktop._desktop_exe_integrity_error", return_value=None), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.main.subprocess.Popen",
side_effect=[spawn_error, None]) as mock_popen, \
pytest.raises(OSError) as exc:
cli_main.cmd_gui(_ns(skip_build=True))
assert exc.value.winerror == 2
# Only the first (breakaway) attempt ran; no doomed retry masked the error.
assert mock_popen.call_count == 1
@pytest.mark.platforms("macos", "linux")
def test_gui_foreground_launch_ctrl_c_exits_cleanly(tmp_path, monkeypatch, capsys):
"""Ctrl-C during the attached launch is a clean close, not a traceback (#59848).
On the foreground platforms the launcher intentionally stays attached to the
Electron child; a KeyboardInterrupt raised through subprocess.run must exit
0 with a short message instead of the raw traceback the reporter saw (which,
unhandled, aborts the whole CLI process).
"""
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
packaged_exe = _make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
def _interrupted_launch(*call_args, **kwargs):
raise KeyboardInterrupt()
with patch("hermes_cli.source_build.prepare_source_dependencies", return_value=ok), \
patch("hermes_cli.main_desktop._desktop_build_needed", return_value=False), \
patch("hermes_cli.main_desktop._desktop_exe_integrity_error", return_value=None), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.main.subprocess.run", side_effect=_interrupted_launch) as mock_run, \
patch("hermes_cli.main.subprocess.Popen") as mock_popen, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns(skip_build=True))
assert exc.value.code == 0
mock_popen.assert_not_called()
assert mock_run.call_count == 1
assert mock_run.call_args.args[0][0] == str(packaged_exe)
assert "closed" in capsys.readouterr().out.lower()
# ---------------------------------------------------------------------------
# #86443: stage-and-swap — a failed Desktop rebuild must never remove the
# working app. electron-builder packs IN PLACE (before-pack.mjs wipes