Files
hermes-agent/tests/pm/test_activate_scripts.py
2026-10-10 15:20:13 +02:00

197 lines
6.9 KiB
Python

"""``activate`` / ``activate.ps1``: an interactive shell takes on the environment and gives it back.
What a process launched under ``scripts/run-in-hermes-env`` sees is covered in
tests/scripts/test_run_in_hermes_env.py; how the environment is composed, in
test_environment_script.py; the fish port, in test_activate_fish.py.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from tests.pm.activation_support import (
ACTIVATE, ACTIVATE_PS1, CANARY, SETUP_HERMES_PS1, SETUP_HERMES_SH, bash, bash_env,
child_env, fake_store, isolated_checkout, posix, powershell, spawnable_python,
)
def test_bash_scripts_pass_syntax_check():
for script in (ACTIVATE, SETUP_HERMES_SH):
result = subprocess.run(
[bash(), "-n", posix(script)], capture_output=True, text=True, env=child_env(),
check=False,
)
assert result.returncode == 0, f"{script.name}: {result.stderr}"
@pytest.mark.platforms("windows")
def test_source_activate_exports_the_pm_env(tmp_path: Path):
root = isolated_checkout(tmp_path)
store, _ = fake_store(tmp_path)
script = (
f'source "{posix(root / "activate")}" && '
f'test -n "$__HERMES_ACTIVATED" && '
f'printf "%s" "${CANARY}"'
)
result = subprocess.run(
[bash(), "-c", script],
capture_output=True,
text=True,
cwd=posix(tmp_path),
env=bash_env(store),
check=False,
)
assert result.returncode == 0, result.stderr
assert result.stdout == "env-ok"
@pytest.mark.platforms("windows")
def test_activate_exports_the_sentinel_to_child_processes(tmp_path: Path):
"""Repo scripts read activation from the environment, so the sentinel must
survive into an exec'd child (a plain shell variable would not), and
deactivate must take it back out."""
root = isolated_checkout(tmp_path)
store, _ = fake_store(tmp_path)
script = (
f'source "{posix(root / "activate")}" && '
f'"$BASH" -c \'test -n "$__HERMES_ACTIVATED"\' && '
f'deactivate && '
f'! "$BASH" -c \'test -n "$__HERMES_ACTIVATED"\' && '
f'echo exported-then-cleared'
)
result = subprocess.run(
[bash(), "-c", script],
capture_output=True,
text=True,
cwd=posix(tmp_path),
env=bash_env(store),
check=False,
)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "exported-then-cleared"
@pytest.mark.platforms("windows")
def test_deactivate_restores_the_prior_shell(tmp_path: Path):
root = isolated_checkout(tmp_path)
store, _ = fake_store(tmp_path)
script = (
f'source "{posix(root / "activate")}" && deactivate && '
f'test -z "${{{CANARY}+set}}" && '
f'test -z "${{__HERMES_ACTIVATED+set}}" && '
f"! declare -F deactivate >/dev/null && "
f'echo restored'
)
result = subprocess.run(
[bash(), "-c", script],
capture_output=True,
text=True,
cwd=posix(tmp_path),
env=bash_env(store),
check=False,
)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "restored"
@pytest.mark.platforms("windows")
def test_activate_leaves_the_shell_paths_in_posix_form(tmp_path: Path):
"""The pm env is read by a native Windows Python, which sees PATH, HOME and
the temp variables in Windows form. Exported verbatim, `C:\\a;C:\\b` left
bash without a usable PATH, so every command after activation failed."""
root = isolated_checkout(tmp_path)
store, _ = fake_store(tmp_path)
script = (
'prior_home="$HOME" prior_tmp="$TMP" && '
f'source "{posix(root / "activate")}" && '
'command -v basename >/dev/null && '
'case "$PATH" in *";"*|*"\\\\"*) exit 3;; esac && '
'test "$HOME" = "$prior_home" && test "$TMP" = "$prior_tmp" && '
'echo posix'
)
result = subprocess.run(
[bash(), "-c", script],
capture_output=True,
text=True,
cwd=posix(tmp_path),
env=bash_env(store),
check=False,
)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "posix"
def test_activate_fails_cleanly_without_a_store(tmp_path: Path):
env = bash_env(tmp_path / "empty-store")
isolated = isolated_checkout(tmp_path) / "activate"
script = (
f'source "{posix(isolated)}" 2>/dev/null; '
f'test $? -ne 0 && echo refused'
)
result = subprocess.run(
[bash(), "-c", script],
capture_output=True,
text=True,
cwd=posix(tmp_path),
env=env,
check=False,
)
# Without any provisioned python the source must refuse — never
# silently no-op with a half-activated shell.
assert "refused" in result.stdout
def test_powershell_scripts_parse():
"""Parse-check the PowerShell entry points; skip gracefully when no
PowerShell host is available."""
ps = powershell()
if ps is None:
pytest.skip("no PowerShell host available")
for script in (ACTIVATE_PS1, SETUP_HERMES_PS1):
result = subprocess.run(
[
ps,
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
f"$errs = $null; $null = [System.Management.Automation.Language.Parser]::ParseFile("
f"'{script}', [ref]$null, [ref]$errs); "
f"if ($errs.Count) {{ $errs | ForEach-Object {{ $_.Message }}; exit 1 }}",
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, f"{script.name}: {result.stdout}{result.stderr}"
@pytest.mark.platforms("windows")
def test_powershell_activate_exports_and_deactivates(tmp_path: Path):
# Native venv redirectors resolve the base DLLs/stdlib without copying CPython.
root = isolated_checkout(tmp_path)
env = bash_env(tmp_path / "store")
subprocess.run(
[str(spawnable_python()), "-m", "venv", "--without-pip", str(root / ".venv")],
check=True, capture_output=True, env=env, timeout=60,
)
ps = powershell()
assert ps, "native Windows test requires PowerShell"
result = subprocess.run(
[ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
f"$ErrorActionPreference='Stop'; $env:PYTHONPATH='caller-original'; "
f". '{root / 'activate.ps1'}'; "
"Write-Output ('active=' + $env:PYTHONPATH); deactivate; "
"Write-Output ('after=' + $env:PYTHONPATH)"],
capture_output=True, text=True, cwd=str(tmp_path), env=env, timeout=40,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
# Nothing is committed, so the checkout alone: the bootstrap .venv's packages never leak in.
assert f"active={root}\n" in result.stdout
assert "after=caller-original" in result.stdout