122 lines
4.6 KiB
Python
122 lines
4.6 KiB
Python
"""Enforcement for the "no visible terminal on Windows" invariant.
|
|
|
|
Windows console-subsystem programs (``taskkill``, ``schtasks``, ``agent-browser``,
|
|
``git-bash`` …) pop a console window unless spawned with ``CREATE_NO_WINDOW``.
|
|
Relying on each call site to remember the flag is how cron-driven and future
|
|
spawns leaked terminal windows. The durable fix is a single chokepoint —
|
|
``hermes_cli._subprocess_compat.run`` / ``.popen`` — that always injects the
|
|
flag on Windows, plus the ``FreeConsole`` catch-all in ``hermes_bootstrap`` for
|
|
Python children.
|
|
|
|
These tests pin both halves of that contract:
|
|
|
|
1. The primitive actually injects ``CREATE_NO_WINDOW`` (and merges, so detach
|
|
callers still work).
|
|
2. No source file spawns a known console exe with a *raw* ``subprocess`` call,
|
|
which would bypass the primitive and reintroduce the window.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import _subprocess_compat
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
_CREATE_NO_WINDOW = 0x08000000
|
|
|
|
|
|
class TestPrimitiveInjectsNoWindow:
|
|
def test_run_injects_create_no_window_on_windows(self, monkeypatch):
|
|
captured: dict = {}
|
|
monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
_subprocess_compat.subprocess, "run", lambda cmd, **kw: captured.update(kw) or "ok"
|
|
)
|
|
|
|
_subprocess_compat.run(["taskkill"], timeout=5)
|
|
|
|
assert captured["creationflags"] & _CREATE_NO_WINDOW
|
|
|
|
def test_popen_injects_create_no_window_on_windows(self, monkeypatch):
|
|
captured: dict = {}
|
|
monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
_subprocess_compat.subprocess, "Popen", lambda cmd, **kw: captured.update(kw) or "ok"
|
|
)
|
|
|
|
_subprocess_compat.popen(["agent-browser"])
|
|
|
|
assert captured["creationflags"] & _CREATE_NO_WINDOW
|
|
|
|
def test_merges_with_existing_detach_flags(self, monkeypatch):
|
|
captured: dict = {}
|
|
monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
_subprocess_compat.subprocess, "run", lambda cmd, **kw: captured.update(kw) or "ok"
|
|
)
|
|
|
|
detach = _subprocess_compat.windows_detach_flags()
|
|
_subprocess_compat.run(["x"], creationflags=detach)
|
|
|
|
assert captured["creationflags"] & _CREATE_NO_WINDOW
|
|
assert captured["creationflags"] & detach == detach
|
|
|
|
def test_no_op_on_posix(self, monkeypatch):
|
|
captured: dict = {}
|
|
monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", False)
|
|
monkeypatch.setattr(
|
|
_subprocess_compat.subprocess, "run", lambda cmd, **kw: captured.update(kw) or "ok"
|
|
)
|
|
|
|
_subprocess_compat.run(["x"])
|
|
|
|
assert "creationflags" not in captured
|
|
|
|
|
|
# Windows-only console tools — they have no POSIX use, so a raw ``subprocess``
|
|
# spawn is unambiguously a Windows path that flashes a terminal. Banning them
|
|
# repo-wide is a pure win (cross-platform tools like git/ffmpeg/node are NOT
|
|
# listed: they have legitimate foreground/POSIX uses a blanket ban would break;
|
|
# their Windows-background call sites are routed through the primitive instead).
|
|
# ``_subprocess_compat.run/.popen`` calls never match these (different prefix).
|
|
_WINDOWS_ONLY_CONSOLE_EXES = ("taskkill", "schtasks", "wmic", "netstat", "tasklist")
|
|
_RAW_CONSOLE_SPAWNS = [
|
|
re.compile(rf"""subprocess\.(?:run|Popen|call)\(\s*\[\s*["']{exe}["']""")
|
|
for exe in _WINDOWS_ONLY_CONSOLE_EXES
|
|
]
|
|
|
|
# The primitive itself is allowed to call raw subprocess — it IS the chokepoint.
|
|
_ALLOWED = {REPO_ROOT / "hermes_cli" / "_subprocess_compat.py"}
|
|
|
|
|
|
# Dev/CI tooling that never ships to a user's Windows desktop, where a flashing
|
|
# console is irrelevant and importing hermes_cli would be inappropriate.
|
|
_SKIP_DIRS = {"tests", "node_modules", ".venv", "venv", "scripts"}
|
|
|
|
|
|
def _python_sources():
|
|
for path in REPO_ROOT.rglob("*.py"):
|
|
if _SKIP_DIRS & set(path.parts):
|
|
continue
|
|
if path in _ALLOWED:
|
|
continue
|
|
yield path
|
|
|
|
|
|
@pytest.mark.parametrize("pattern", _RAW_CONSOLE_SPAWNS, ids=_WINDOWS_ONLY_CONSOLE_EXES)
|
|
def test_no_raw_console_exe_spawns(pattern):
|
|
offenders = [
|
|
str(path.relative_to(REPO_ROOT))
|
|
for path in _python_sources()
|
|
if pattern.search(path.read_text(encoding="utf-8", errors="ignore"))
|
|
]
|
|
|
|
assert not offenders, (
|
|
"Console-subsystem exe spawned via raw subprocess (flashes a terminal on "
|
|
f"Windows). Route through hermes_cli._subprocess_compat.run/.popen instead: {offenders}"
|
|
)
|