Files
hermes-agent/tests/scripts/test_termux_launchers.py
ethernet cb7c688171 test: mirror the source tree for 27 misfiled root tests; one PM home fixture
Root-level tests/ is for root-level modules; 19 files testing scripts/, 3 testing
pm/ and 5 testing hermes_cli/ move to the mirrored directory (workflow file lists
and cross-imports updated; path arithmetic bumped one level).

tests/pm gains a conftest with the isolated_machine_home fixture that seven
modules had copy-pasted verbatim.
2026-09-18 20:13:26 -04:00

105 lines
4.3 KiB
Python

"""Run generated launchers with a real interpreter and captured entrypoint."""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
from scripts.build.launchers import write_launchers
@pytest.mark.platforms("posix")
def test_launchers_forward_arguments_and_export_payload_environment(tmp_path):
payload = tmp_path / "payload with spaces"
(payload / "venv/bin").mkdir(parents=True)
(payload / "venv/bin/python").symlink_to(sys.executable)
(payload / "app").mkdir()
(payload / "app/capture_entry.py").write_text(
"import json, os, sys\n"
"def main():\n"
" print(json.dumps({'argv': sys.argv[1:], 'env': {k: os.environ.get(k) for k in "
"['HERMES_NODE', 'HERMES_PYTHON', 'HERMES_RUNTIME_DIR', 'PYTHONPATH', 'PYTHONHOME', 'PYTHONPYCACHEPREFIX']}}))\n",
encoding="utf-8",
)
(tmp_path / "json.py").write_text("raise RuntimeError('cwd shadowed stdlib')\n", encoding="utf-8")
entries = {name: "capture_entry:main" for name in ("hermes", "hermes-agent", "hermes-acp")}
write_launchers(payload, entries, python="venv/bin/python", repo="app",
site="venv/lib/python3.14/site-packages", target="linux-arm64-bionic")
bin_dir = tmp_path / "prefix/bin"
bin_dir.mkdir(parents=True)
for name in entries:
link = bin_dir / name
link.symlink_to(os.path.relpath(payload / "bin" / name, bin_dir))
result = subprocess.run(
[shutil.which("sh"), str(link), "one two", "$(not-executed)", ""],
cwd=tmp_path, check=True, capture_output=True, text=True,
env={**os.environ, "PYTHONHOME": "/does-not-exist", "PYTHONPATH": "/foreign"},
)
data = json.loads(result.stdout)
assert data["argv"] == ["one two", "$(not-executed)", ""]
env = data["env"]
assert Path(env["HERMES_PYTHON"]).resolve() == Path(sys.executable).resolve()
assert Path(env["HERMES_NODE"]) == payload / "tools/node/data/data/com.termux/files/usr/bin/node"
assert Path(env["HERMES_RUNTIME_DIR"]) == payload / "tools"
assert env["PYTHONPATH"].split(os.pathsep)[0] == str(payload / "app")
assert env["PYTHONHOME"] is None
assert not Path(env["PYTHONPYCACHEPREFIX"]).is_relative_to(payload)
@pytest.mark.platforms("posix")
def test_postinst_refuses_foreign_path_and_prerm_preserves_it(tmp_path):
from scripts.termux.launchers import write_maintainer_scripts
control = tmp_path / "DEBIAN"
control.mkdir()
write_maintainer_scripts(control, ["hermes"])
prefix = tmp_path / "prefix"
bin_dir = prefix / "bin"
bin_dir.mkdir(parents=True)
link = bin_dir / "hermes"
env = {**os.environ, "PREFIX": str(prefix)}
command = [shutil.which("sh"), str(control / "postinst"), "configure"]
subprocess.run(command, check=True, env=env)
assert os.readlink(link) == "../lib/hermes-agent/bin/hermes"
subprocess.run(command, check=True, env=env)
link.unlink()
link.write_text("foreign launcher", encoding="utf-8")
assert subprocess.run(command, env=env).returncode != 0
subprocess.run([shutil.which("sh"), str(control / "prerm"), "remove"], check=True, env=env)
assert link.read_text(encoding="utf-8") == "foreign launcher"
@pytest.mark.platforms("posix")
def test_verifier_stops_detached_descendants_before_cleanup(tmp_path):
import psutil
from scripts.termux.validate_installed import stop_child_tree
code = (
"import subprocess, sys, time\n"
"child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(120)'], "
"start_new_session=True)\n"
"print(child.pid, flush=True)\n"
"time.sleep(120)\n"
)
child = subprocess.Popen(
[sys.executable, "-c", code], stdout=subprocess.PIPE, text=True,
cwd=tmp_path, start_new_session=True,
)
descendant = psutil.Process(int(child.stdout.readline()))
try:
stop_child_tree(child)
assert child.poll() is not None
assert not descendant.is_running() or descendant.status() == psutil.STATUS_ZOMBIE
finally:
if child.poll() is None:
child.kill()
if descendant.is_running() and descendant.status() != psutil.STATUS_ZOMBIE:
descendant.kill()
child.wait(timeout=10)
child.stdout.close()