Files
hermes-agent/tests/scripts/test_hermes_python_prologue.py
ethernet b16f953f7c fix(pm): re-activate from the shebang only when an input's mtime differs
The _hermes-python prologue compared uv.lock / pyproject.toml / pm/lock.json
against facts.json with `-nt`. facts.json is only rewritten on a real sync,
so a checkout that rewrites an input without changing it left the input
newer forever: every run from an activated shell re-sourced activate
(~370ms instead of ~13ms).

pm now records, after every successful full-closure install (no-op syncs
included), a stamp per input under installs/<key>/inputs carrying the exact
mtime the install was verified against, snapshotted before installing so
an input edited mid-install still reads as stale. The prologue re-activates
when any input's mtime differs from its stamp in either direction (branch
switches can move it backwards), when an input is missing, or when there
are no stamps. It globs the stamp dir, so pm owns the only input list.

Also read pyvenv.cfg as utf-8-sig (footgun lane, same file).
2026-09-23 13:38:54 -04:00

144 lines
5.6 KiB
Python

"""The self-activating shebang prologue: when does it re-activate?
``scripts/_hermes-python`` is the POSIX shebang target. Its one decision worth
pinning is staleness: pm stamps each dependency input's mtime beside the
installed-state file ``__HERMES_ACTIVATED`` names, and any input whose mtime
differs from its stamp means the inherited environment may not match its
inputs. Invert that either way and the cost is invisible — a re-sync on every
run, or a stale environment that looks fine.
The stamps come from the real ``pm.environments`` writer, so these tests pin
the contract between what pm records and what the prologue reads.
A ``python3`` shim goes on PATH because the prologue execs ``python3`` by name,
which does not exist on stock Windows; the subject here is the staleness
branch, not interpreter resolution.
"""
from __future__ import annotations
import datetime
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
from pm.environments import ACTIVATION_INPUTS, activation_input_mtimes, record_activation_inputs
REPO_ROOT = Path(__file__).resolve().parents[2]
PROLOGUE = REPO_ROOT / "scripts" / "_hermes-python"
EARLIER = "2018-01-01 00:00:00"
LONG_AGO = "2019-01-01 00:00:00"
STAMP_TIME = "2020-06-01 00:00:00"
JUST_AFTER = "2021-01-01 00:00:00"
def _posix(path: Path) -> str:
return str(path).replace("\\", "/")
def _bash() -> str:
found = shutil.which("bash")
if found and "windowsapps" not in str(found).lower():
return found
if sys.platform == "win32":
for rel in (("Git", "bin", "bash.exe"), ("Git", "usr", "bin", "bash.exe")):
cand = Path(os.environ.get("ProgramFiles", r"C:\Program Files")).joinpath(*rel)
if cand.exists():
return str(cand)
return found or "bash"
def _set_mtime(path: Path, stamp: str) -> None:
when = datetime.datetime.strptime(stamp, "%Y-%m-%d %H:%M:%S").timestamp()
os.utime(path, (when, when))
@pytest.fixture
def checkout(tmp_path: Path) -> Path:
"""A repo-shaped tree whose stub activate announces each sourcing; its
installed state lives in ``state/`` (facts.json beside the input stamps)."""
root = tmp_path / "checkout"
(root / "scripts").mkdir(parents=True)
(root / "shim").mkdir()
(root / "state").mkdir()
shutil.copy2(PROLOGUE, root / "scripts" / PROLOGUE.name)
for name in ACTIVATION_INPUTS:
(root / name).parent.mkdir(parents=True, exist_ok=True)
(root / name).touch()
_set_mtime(root / name, LONG_AGO)
(root / "state" / "facts.json").touch()
_set_mtime(root / "state" / "facts.json", STAMP_TIME)
# Quote in posix form: a /bin/sh script treats backslashes in an unquoted
# word as escapes (same pattern as tests/pm/test_activate_scripts.py).
shim = root / "shim" / "python3"
shim.write_text("#!/bin/sh\nexec '%s' \"$@\"\n" % _posix(Path(sys.executable)), encoding="utf-8")
shim.chmod(0o755)
(root / "scripts" / "probe.py").write_text(
"import os, sys\n"
"print('target ran; sentinel =', os.environ.get('__HERMES_ACTIVATED'))\n",
encoding="utf-8",
)
(root / "activate").write_text(
"echo 'ACTIVATED' >&2\n"
"export __HERMES_ACTIVATED=\"%s/state/facts.json\"\n"
"export PATH=\"%s/shim:$PATH\"\n" % (_posix(root), _posix(root)),
encoding="utf-8",
)
return root
def _record(root: Path) -> None:
"""What a successful ``pm install`` leaves behind."""
record_activation_inputs(root / "state" / "inputs", activation_input_mtimes(root))
def _run(root: Path, sentinel: str | None = "{root}/state/facts.json") -> str:
"""Drive the prologue as the kernel would; return stderr, assert it ran."""
env = {**os.environ, "PATH": f"{_posix(root / 'shim')}{os.pathsep}{os.environ.get('PATH', '')}"}
env.pop("__HERMES_ACTIVATED", None)
if sentinel is not None:
env["__HERMES_ACTIVATED"] = sentinel.replace("{root}", _posix(root))
result = subprocess.run(
[_bash(), _posix(root / "scripts" / PROLOGUE.name), _posix(root / "scripts" / "probe.py")],
capture_output=True, text=True, cwd=_posix(root), env=env, timeout=60,
)
assert result.returncode == 0, result.stderr
assert "target ran" in result.stdout, result.stderr
return result.stderr
def test_recorded_inputs_are_left_alone(checkout: Path):
"""A checkout rewrote every input after facts.json was last written, then a
no-op sync recorded them: the environment is current. Re-syncing on every
run is the cost of getting this wrong."""
for name in ACTIVATION_INPUTS:
_set_mtime(checkout / name, JUST_AFTER)
_record(checkout)
assert "ACTIVATED" not in _run(checkout)
@pytest.mark.parametrize("moved_to", [JUST_AFTER, EARLIER], ids=["newer", "older"])
@pytest.mark.parametrize("input_name", ACTIVATION_INPUTS)
def test_input_mtime_differing_from_its_stamp_reactivates(checkout: Path, input_name: str, moved_to: str):
"""A branch switch can move an input's mtime either way; both mean the
recorded install was not verified against what is on disk now."""
_record(checkout)
_set_mtime(checkout / input_name, moved_to)
assert "ACTIVATED" in _run(checkout)
@pytest.mark.parametrize("sentinel", [None, "{root}/gone", "1", "{root}/state/facts.json"],
ids=["cold", "dangling", "legacy-1", "no-stamps"])
def test_unusable_sentinel_activates(checkout: Path, sentinel: str | None):
"""Cold, dangling stamp, the bare ``1`` an older activate exported, or an
install from before stamps existed — each must activate rather than read
as current."""
assert "ACTIVATED" in _run(checkout, sentinel)