mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-16 20:50:18 +00:00
6f68fdb6ff
## Release v0.51.236 — Release HD (stage-q7) First Phase 3 (deep-review) release — picked by the 3-factor framework (contributor × impact × mitigated-risk): high-impact (#1952 native Windows support), backend-only (no screenshots), well-mitigated risk (POSIX path provably unchanged), from a contributor active this session (@rodboev, #3446/#3486 shipped earlier today). ### Added | PR | Author | Fix | |----|--------|-----| | #1952 | @rodboev | Native Windows support for `bootstrap.py` + the embedded terminal: POSIX-only `fcntl`/`termios`/`select` guarded behind `_TERMINAL_SUPPORTED`; terminal entry points raise `NotImplementedError`/no-op on Windows; bootstrap Windows block → warning; auto-install errors clearly on native Windows (WSL unaffected); foreground uses `Popen`+exit on Windows instead of `os.execv`. **POSIX behavior unchanged on every path.** | ### Absorbed on the way in (fix-it-ourselves, reviewed fresh) - `subprocess.CREATE_NEW_PROCESS_GROUP` → `getattr(subprocess, ..., 0)` — the constant is Windows-only, so a win32-simulating test `AttributeError`'d on Linux. Mirrors the `SO_EXCLUSIVEADDRUSE` getattr guard. - Fixed 2 over-reaching tests in `test_windows_native_support.py` — one was launching a **real installer subprocess** via an unstubbed `subprocess.run` (now stubbed; harness 2.8s vs 80s); removed unused imports. - Updated `test_onboarding_static.py` — it asserted the OLD "Native Windows is not supported" hard-block string this PR intentionally replaces; now asserts the new experimental-warning + auto-install guard. - Help-text accuracy: `--foreground` help now describes the Windows Popen path (Opus nit). ### Gate results - **Full pytest suite**: 7478 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN (gate hardened mid-release to auto-detect the cached chromium revision) - **Codex (regression)**: SAFE TO SHIP (simulated `sys.platform=win32`, verified POSIX modules not imported + all terminal guards complete + POSIX foreground still uses execv) - **Opus (correctness)**: SAFE TO SHIP (POSIX path provably unchanged, all fcntl/termios/select guarded, Popen+exit correct; noted inherent-Windows trade-offs that aren't PR bugs) Note: the Windows *runtime* path can't be executed on the Linux CI box; it was reviewed statically by both reviewers + the contributor's 209-line test (win32 simulated via monkeypatch). Linux/POSIX no-regression is fully verified. Closes #1952. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
215 lines
8.8 KiB
Python
215 lines
8.8 KiB
Python
"""Tests for native Windows support (terminal.py POSIX guards + bootstrap.py unblock).
|
|
|
|
terminal.py guards:
|
|
- _TERMINAL_SUPPORTED is False on Windows, True on POSIX
|
|
- Terminal functions raise NotImplementedError on Windows
|
|
- close_terminal returns False (no-op) on Windows
|
|
- get_terminal returns None on Windows
|
|
- Module imports cleanly on Windows (no fcntl/termios ImportError)
|
|
|
|
bootstrap.py unblock:
|
|
- ensure_supported_platform does not raise on Windows
|
|
- install_hermes_agent raises RuntimeError on Windows (calls /bin/bash)
|
|
- Foreground path uses Popen + sys.exit on Windows instead of os.execv
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
|
|
# ── terminal.py tests ────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTerminalPosixGuard:
|
|
"""Verify _TERMINAL_SUPPORTED flag and import guards."""
|
|
|
|
def test_terminal_supported_matches_platform(self):
|
|
from api import terminal
|
|
if sys.platform == "win32":
|
|
assert not terminal._TERMINAL_SUPPORTED
|
|
else:
|
|
assert terminal._TERMINAL_SUPPORTED
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only check")
|
|
def test_posix_modules_are_loaded(self):
|
|
from api import terminal
|
|
assert terminal.fcntl is not None
|
|
assert terminal.select is not None
|
|
assert terminal.termios is not None
|
|
|
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only check")
|
|
def test_windows_modules_are_none(self):
|
|
from api import terminal
|
|
assert terminal.fcntl is None
|
|
assert terminal.select is None
|
|
assert terminal.termios is None
|
|
|
|
|
|
class TestTerminalWindowsFunctions:
|
|
"""Terminal functions should fail gracefully on Windows."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _force_unsupported(self, monkeypatch):
|
|
"""Force _TERMINAL_SUPPORTED=False regardless of actual platform."""
|
|
from api import terminal
|
|
monkeypatch.setattr(terminal, "_TERMINAL_SUPPORTED", False)
|
|
|
|
def test_start_terminal_raises(self, tmp_path):
|
|
from api.terminal import start_terminal
|
|
with pytest.raises(NotImplementedError, match="not supported on Windows"):
|
|
start_terminal("test-session", tmp_path)
|
|
|
|
def test_write_terminal_raises(self):
|
|
from api.terminal import write_terminal
|
|
with pytest.raises(NotImplementedError, match="not supported on Windows"):
|
|
write_terminal("test-session", "hello")
|
|
|
|
def test_resize_terminal_raises(self):
|
|
from api.terminal import resize_terminal
|
|
with pytest.raises(NotImplementedError, match="not supported on Windows"):
|
|
resize_terminal("test-session", 24, 80)
|
|
|
|
def test_close_terminal_returns_false(self):
|
|
from api.terminal import close_terminal
|
|
assert close_terminal("test-session") is False
|
|
|
|
def test_get_terminal_returns_none(self):
|
|
from api.terminal import get_terminal
|
|
assert get_terminal("test-session") is None
|
|
|
|
|
|
# ── bootstrap.py tests ───────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture
|
|
def import_bootstrap():
|
|
"""Import bootstrap freshly to avoid module-level state bleed."""
|
|
if "bootstrap" in sys.modules:
|
|
del sys.modules["bootstrap"]
|
|
import bootstrap as bs
|
|
return bs
|
|
|
|
|
|
class TestBootstrapWindowsUnblock:
|
|
|
|
def test_ensure_supported_platform_does_not_raise_on_windows(self, import_bootstrap, monkeypatch):
|
|
monkeypatch.setattr("platform.system", lambda: "Windows")
|
|
monkeypatch.setattr(import_bootstrap, "is_wsl", lambda: False)
|
|
# Should print a warning, not raise
|
|
import_bootstrap.ensure_supported_platform()
|
|
|
|
def test_ensure_supported_platform_still_passes_on_posix(self, import_bootstrap, monkeypatch):
|
|
monkeypatch.setattr("platform.system", lambda: "Linux")
|
|
import_bootstrap.ensure_supported_platform()
|
|
|
|
def test_install_hermes_agent_raises_on_windows(self, import_bootstrap, monkeypatch):
|
|
monkeypatch.setattr("platform.system", lambda: "Windows")
|
|
monkeypatch.setattr(import_bootstrap, "is_wsl", lambda: False)
|
|
with pytest.raises(RuntimeError, match="not supported on native Windows"):
|
|
import_bootstrap.install_hermes_agent()
|
|
|
|
def test_install_hermes_agent_allowed_in_wsl(self, import_bootstrap, monkeypatch):
|
|
"""WSL should still be able to auto-install (it has /bin/bash) — the
|
|
Windows guard must NOT fire. Stub subprocess.run so the test proves the
|
|
guard was passed without actually launching the real installer."""
|
|
monkeypatch.setattr("platform.system", lambda: "Windows")
|
|
monkeypatch.setattr(import_bootstrap, "is_wsl", lambda: True)
|
|
run_calls = []
|
|
monkeypatch.setattr(
|
|
import_bootstrap.subprocess, "run",
|
|
lambda *a, **kw: run_calls.append((a, kw)),
|
|
)
|
|
# Must NOT raise the native-Windows guard; instead it reaches the
|
|
# (stubbed) install subprocess.
|
|
import_bootstrap.install_hermes_agent()
|
|
assert len(run_calls) == 1, "WSL install should reach the install subprocess"
|
|
|
|
|
|
class TestBootstrapForegroundWindows:
|
|
"""Foreground path uses Popen + sys.exit on Windows instead of os.execv."""
|
|
|
|
@pytest.fixture
|
|
def stub_main_dependencies(self, monkeypatch, tmp_path):
|
|
import bootstrap as bs
|
|
monkeypatch.setattr(bs, "ensure_supported_platform", lambda: None)
|
|
monkeypatch.setattr(bs, "discover_agent_dir", lambda: tmp_path / "agent")
|
|
monkeypatch.setattr(bs, "hermes_command_exists", lambda: True)
|
|
python_exe = sys.executable
|
|
monkeypatch.setattr(bs, "discover_launcher_python", lambda *a: python_exe)
|
|
monkeypatch.setattr(bs, "ensure_python_has_webui_deps", lambda *a, **kw: a[0])
|
|
monkeypatch.setattr(bs, "wait_for_health", lambda *a, **kw: True)
|
|
monkeypatch.setattr(bs, "open_browser", lambda *a, **kw: None)
|
|
monkeypatch.setenv("HERMES_WEBUI_STATE_DIR", str(tmp_path / "state"))
|
|
(tmp_path / "agent").mkdir(parents=True, exist_ok=True)
|
|
return bs
|
|
|
|
def test_foreground_uses_popen_on_windows(self, stub_main_dependencies, monkeypatch):
|
|
"""On win32, foreground mode should use Popen + sys.exit, not os.execv."""
|
|
bs = stub_main_dependencies
|
|
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground"])
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(os, "chdir", lambda p: None)
|
|
|
|
# Strip supervisor env vars that would interfere
|
|
for var in ("INVOCATION_ID", "JOURNAL_STREAM", "NOTIFY_SOCKET",
|
|
"XPC_SERVICE_NAME", "SUPERVISOR_ENABLED", "HERMES_WEBUI_FOREGROUND"):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
popen_calls = []
|
|
execv_calls = []
|
|
|
|
class FakePopen:
|
|
pid = 12345
|
|
def __init__(self, *args, **kwargs):
|
|
popen_calls.append((args, kwargs))
|
|
|
|
monkeypatch.setattr(subprocess, "Popen", FakePopen)
|
|
monkeypatch.setattr(os, "execv", lambda *a: execv_calls.append(a))
|
|
|
|
with pytest.raises(SystemExit) as ei:
|
|
bs.main()
|
|
assert ei.value.code == 0
|
|
assert len(popen_calls) == 1, "Windows foreground should use Popen"
|
|
assert len(execv_calls) == 0, "Windows foreground must NOT use execv"
|
|
|
|
def test_foreground_uses_execv_on_posix(self, stub_main_dependencies, monkeypatch):
|
|
"""On POSIX, foreground mode should still use os.execv."""
|
|
bs = stub_main_dependencies
|
|
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground"])
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
monkeypatch.setattr(os, "chdir", lambda p: None)
|
|
|
|
for var in ("INVOCATION_ID", "JOURNAL_STREAM", "NOTIFY_SOCKET",
|
|
"XPC_SERVICE_NAME", "SUPERVISOR_ENABLED", "HERMES_WEBUI_FOREGROUND"):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
execv_calls = []
|
|
popen_calls = []
|
|
|
|
def fake_execv(path, argv):
|
|
execv_calls.append((path, argv))
|
|
raise SystemExit(0)
|
|
|
|
monkeypatch.setattr(os, "execv", fake_execv)
|
|
|
|
class FakePopen:
|
|
pid = 12345
|
|
def __init__(self, *args, **kwargs):
|
|
popen_calls.append((args, kwargs))
|
|
|
|
monkeypatch.setattr(subprocess, "Popen", FakePopen)
|
|
|
|
with pytest.raises(SystemExit) as ei:
|
|
bs.main()
|
|
assert ei.value.code == 0
|
|
assert len(execv_calls) == 1, "POSIX foreground should use execv"
|
|
assert len(popen_calls) == 0, "POSIX foreground must NOT use Popen"
|