Files
hermes-agent/tests/scripts/install/test_install_ps1_managed_python_provenance.py
teknium1 d10bb2ab6f test: make tests/ mirror the source tree; drop issue numbers from filenames
`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.

Parallel directories for one source package, folded into the mirror:
  tests/acp        -> tests/acp_adapter   (its __init__/conftest move with it)
  tests/cli        -> tests/hermes_cli    (prompt_toolkit fixture merged into
                                           hermes_cli/conftest.py)
  tests/run_agent  -> tests/agent         (backoff fixture becomes
                                           agent/conftest.py)
  tests/relay      -> tests/gateway/relay
  tests/state      -> tests/hermes_state

246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.

Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.

Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).

Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.
2026-09-13 09:18:02 -07:00

270 lines
8.5 KiB
Python

"""Behavioral regression for Hermes-managed Python provenance on Windows."""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import pytest
from tests.install_ps1_fake_uv import compile_fake_uv
pytestmark = pytest.mark.windows_only
_INSTALL_PS1 = Path(__file__).resolve().parents[3] / "scripts" / "install.ps1"
def test_fresh_install_manifest_orders_repo_before_checkout_scoped_python(
tmp_path: Path,
) -> None:
powershell = shutil.which("powershell")
if not powershell:
pytest.skip("Windows PowerShell is required")
install_dir = tmp_path / "install"
run = subprocess.run(
[
powershell,
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
str(_INSTALL_PS1),
"-Manifest",
"-HermesHome",
str(tmp_path / "hermes-home"),
"-InstallDir",
str(install_dir),
],
cwd=tmp_path,
capture_output=True,
text=True,
check=False,
timeout=45,
)
assert run.returncode == 0, run.stdout + run.stderr
manifest = json.loads(run.stdout)
stages = [stage["name"] for stage in manifest["stages"]]
assert (
stages.index("repository")
< stages.index("python")
< stages.index("venv")
)
assert not install_dir.exists(), "manifest lookup must remain read-only"
def _run_venv_stage(
powershell: str,
tmp_path: Path,
hermes_home: Path,
install_dir: Path,
env: dict[str, str],
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
powershell,
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
str(_INSTALL_PS1),
"-Stage",
"venv",
"-HermesHome",
str(hermes_home),
"-InstallDir",
str(install_dir),
],
cwd=tmp_path,
env=env,
capture_output=True,
text=True,
check=False,
timeout=45,
)
def test_venv_stage_rejects_third_party_python_and_uses_managed_path(
tmp_path: Path,
) -> None:
powershell = shutil.which("powershell")
if not powershell:
pytest.skip("Windows PowerShell is required")
hermes_home = tmp_path / "hermes-home"
install_dir = tmp_path / "install"
managed_root = install_dir / ".hermes-runtime" / "python"
managed_python = managed_root / "cpython-3.11" / "python.exe"
third_party = tmp_path / "KiCad" / "bin" / "python.exe"
log = tmp_path / "uv.log"
uv = hermes_home / "bin" / "uv.exe"
uv.parent.mkdir(parents=True)
install_dir.mkdir()
managed_python.parent.mkdir(parents=True)
third_party.parent.mkdir(parents=True)
third_party.write_text("fake", encoding="ascii")
compile_fake_uv(powershell, uv)
shutil.copy2(uv, managed_python)
env = {
**os.environ,
"FAKE_UV_LOG": str(log),
"FAKE_MANAGED_PYTHON": str(managed_python),
"FAKE_THIRD_PARTY_PYTHON": str(third_party),
"UV_PYTHON": str(third_party),
"UV_NO_MANAGED_PYTHON": "1",
"UV_SYSTEM_PYTHON": "1",
}
run = _run_venv_stage(powershell, tmp_path, hermes_home, install_dir, env)
installer_stdout = run.stdout
installer_stderr = run.stderr
frames = [
json.loads(line) for line in installer_stdout.splitlines() if line.startswith("{")
]
assert run.returncode == 0, installer_stdout + installer_stderr
assert frames[-1]["ok"] is True
commands = log.read_text(encoding="utf-8").splitlines()
assert any(
command.startswith("python find 3.11") and "--managed-python" in command
for command in commands
)
venv_command = next(command for command in commands if command.startswith("venv venv"))
assert f"--python {managed_python}" in venv_command
assert "--managed-python" in venv_command
assert "--no-python-downloads" in venv_command
def test_fallback_minor_is_reported_from_resolved_managed_interpreter(
tmp_path: Path,
) -> None:
powershell = shutil.which("powershell")
if not powershell:
pytest.skip("Windows PowerShell is required")
hermes_home = tmp_path / "hermes-home"
install_dir = tmp_path / "install"
managed_python = (
install_dir / ".hermes-runtime" / "python" / "cpython-3.12" / "python.exe"
)
uv = hermes_home / "bin" / "uv.exe"
uv.parent.mkdir(parents=True)
managed_python.parent.mkdir(parents=True)
install_dir.mkdir(exist_ok=True)
compile_fake_uv(powershell, uv)
shutil.copy2(uv, managed_python)
env = {
**os.environ,
"FAKE_UV_LOG": str(tmp_path / "uv.log"),
"FAKE_MANAGED_PYTHON": str(managed_python),
"FAKE_MANAGED_PYTHON_VERSION": "3.12",
"FAKE_PYTHON_VERSION": "Python 3.12.13",
}
run = _run_venv_stage(powershell, tmp_path, hermes_home, install_dir, env)
assert run.returncode == 0, run.stdout + run.stderr
assert "Creating virtual environment with Python 3.12" in run.stdout
assert "Virtual environment ready (Python 3.12)" in run.stdout
created_python = install_dir / "venv" / "Scripts" / "python.exe"
version = subprocess.run(
[str(created_python), "--version"],
env=env,
capture_output=True,
text=True,
check=True,
)
assert version.stdout.strip() == "Python 3.12.13"
def test_python_find_drains_large_stderr_without_deadlock(tmp_path: Path) -> None:
powershell = shutil.which("powershell")
if not powershell:
pytest.skip("Windows PowerShell is required")
hermes_home = tmp_path / "hermes-home"
install_dir = tmp_path / "install"
managed_python = (
install_dir / ".hermes-runtime" / "python" / "cpython-3.11" / "python.exe"
)
uv = hermes_home / "bin" / "uv.exe"
uv.parent.mkdir(parents=True)
managed_python.parent.mkdir(parents=True)
compile_fake_uv(powershell, uv)
shutil.copy2(uv, managed_python)
env = {
**os.environ,
"FAKE_UV_LOG": str(tmp_path / "uv.log"),
"FAKE_MANAGED_PYTHON": str(managed_python),
"FAKE_UV_FIND_STDERR_BYTES": str(1024 * 1024),
}
run = _run_venv_stage(powershell, tmp_path, hermes_home, install_dir, env)
assert run.returncode == 0, run.stdout + run.stderr
assert "Creating virtual environment with Python 3.11" in run.stdout
def test_python_find_timeout_kills_uv_and_fails_stage(tmp_path: Path) -> None:
powershell = shutil.which("powershell")
if not powershell:
pytest.skip("Windows PowerShell is required")
hermes_home = tmp_path / "hermes-home"
install_dir = tmp_path / "install"
uv = hermes_home / "bin" / "uv.exe"
uv.parent.mkdir(parents=True)
install_dir.mkdir()
compile_fake_uv(powershell, uv)
env = {
**os.environ,
"FAKE_UV_LOG": str(tmp_path / "uv.log"),
"FAKE_UV_FIND_DELAY_MS": "60000",
}
run = _run_venv_stage(powershell, tmp_path, hermes_home, install_dir, env)
assert run.returncode != 0
assert "uv python find 3.11 timed out after 30000 ms" in (run.stdout + run.stderr)
def test_venv_failure_fails_stage_and_restores_existing_environment(
tmp_path: Path,
) -> None:
powershell = shutil.which("powershell")
if not powershell:
pytest.skip("Windows PowerShell is required")
hermes_home = tmp_path / "hermes-home"
install_dir = tmp_path / "install"
managed_python = (
install_dir / ".hermes-runtime" / "python" / "cpython-3.11" / "python.exe"
)
old_python = install_dir / "venv" / "Scripts" / "python.exe"
uv = hermes_home / "bin" / "uv.exe"
for directory in (uv.parent, managed_python.parent, old_python.parent):
directory.mkdir(parents=True, exist_ok=True)
old_python.write_text("previous environment", encoding="ascii")
compile_fake_uv(powershell, uv)
shutil.copy2(uv, managed_python)
env = {
**os.environ,
"FAKE_UV_LOG": str(tmp_path / "uv.log"),
"FAKE_MANAGED_PYTHON": str(managed_python),
"FAKE_UV_VENV_EXIT": "37",
}
run = _run_venv_stage(powershell, tmp_path, hermes_home, install_dir, env)
assert run.returncode != 0
assert "Failed to create virtual environment (uv venv exited with 37)" in (
run.stdout + run.stderr
)
assert old_python.read_text(encoding="ascii") == "previous environment"
frames = [json.loads(line) for line in run.stdout.splitlines() if line.startswith("{")]
assert frames[-1]["ok"] is False