Files
hermes-agent/tests/tools/test_bot_relay_windows_paths.py
ethernet 9f837d298b Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Conflicts resolved toward the PM model: main's lazy_deps/update_cmd_deps/npm
stamp machinery stays deleted (PM + scripts/build/node-deps.mjs own it), the
systemd ExecStop stop-mark rides the installation launcher, legacy
linux_only/macos_only/windows_only markers are rewritten to platforms(), and
finalize_update_receipt carries pending manual-serve obligations forward
again (lost when the ContextVar receipt rewrite crossed c0aa3ce354).

Test harness: the real-home I/O guard exempts /proc/<pid>/fd metadata reads
(deleted-WAL holder scans) and run_tests.sh drops ~/.hermes PATH entries so
shutil.which() cannot trip the tripwire.
2026-09-20 10:07:50 -04:00

120 lines
4.6 KiB
Python

r"""Windows-path viability and venv CLI resolution for bot relay (#93590).
Two failures on a Windows desktop install talking to a remote gateway:
1. ``waiter_command`` used to embed the reply path into generated ``python -c``
source, where the Windows execution layer's backslash folding turned
``C:\\Users`` into a unicode escape and SyntaxErrored the script. The waiter
is now a runner entrypoint (``bot_mode_dm.py --wait-reply``) that takes the
path as argv, rewritten to forward slashes the way the delivery runner's
argv is — the tracked local backend runs commands through Git Bash there.
2. ``local_delivery_command`` hardcoded ``"hermes"``, relying on PATH —
which service contexts (systemd units, desktop launchers, non-login
SSH shells) do not provide, so delivery died with ENOENT. It now
resolves the CLI next to this gateway's own interpreter (the venv
bin/Scripts sibling), falling back to the bare name. The #93091
turn-lock recognition in bot_mode_dm matches the CLI element by
basename so resolved absolute paths (and ``hermes.exe``) still take
the per-profile lock.
"""
import shlex
from pathlib import Path
import pytest
import tools.bot_mode_dm as bot_mode_dm
import tools.bot_relay as bot_relay
import pytest
ENV = {"id": "d" * 32, "target_handle": "researcher", "target_connection": "ssh-vps"}
@pytest.mark.platforms("windows")
def test_waiter_argv_uses_forward_slashes_on_windows():
"""On native Windows the reply path rides as a forward-slash argv element, like the delivery
runner's paths: Git Bash runs those, and parses a backslash path as a command name."""
parts = shlex.split(bot_relay.waiter_command("C:\\Users\\joshu\\.hermes", ENV))
assert "-c" not in parts and "--wait-reply" in parts
assert parts[parts.index("--wait-reply") + 1] == f"C:/Users/joshu/.hermes/bot_relay/replies/{ENV['id']}.json"
assert not any("\\" in part for part in parts)
@pytest.mark.platforms("linux")
def test_local_delivery_resolves_sibling_hermes(tmp_path, monkeypatch):
bin_dir = tmp_path / "venv" / "bin"
bin_dir.mkdir(parents=True)
sibling = bin_dir / "hermes"
sibling.touch()
sibling.chmod(0o755)
monkeypatch.setattr("sys.executable", str(bin_dir / "python"))
argv = bot_relay.local_delivery_command("ops", "query.json")
assert argv[0] == str(sibling)
assert argv[1:3] == ["-p", "ops"]
assert argv[argv.index("--query-file") + 1] == "query.json"
def test_local_delivery_uses_shutil_which_when_no_sibling(tmp_path, monkeypatch):
"""Without a venv sibling, a PATH hit (shutil.which) wins next —
interactive shells keep resolving exactly what they resolve today."""
empty = tmp_path / "nowhere"
empty.mkdir(parents=True)
monkeypatch.setattr("sys.executable", str(empty / "python"))
which_hit = str(tmp_path / "usr-local-bin" / "hermes")
monkeypatch.setattr(
bot_relay.shutil, "which", lambda name: which_hit if name == "hermes" else None
)
argv = bot_relay.local_delivery_command("ops", "query.json")
assert argv[0] == which_hit
def test_local_delivery_falls_back_to_bare_name(tmp_path, monkeypatch):
empty = tmp_path / "nowhere"
empty.mkdir(parents=True)
monkeypatch.setattr("sys.executable", str(empty / "python"))
monkeypatch.setattr(bot_relay.shutil, "which", lambda name: None)
argv = bot_relay.local_delivery_command("ops", "query.json")
assert argv[0] == "hermes"
assert argv[1:3] == ["-p", "ops"]
def test_delivery_lock_recognizes_resolved_cli_paths(tmp_path, monkeypatch):
"""The #93091 per-profile turn lock must keep matching delivery argvs
now that argv[0] may be a resolved absolute path (or hermes.exe)."""
acquired = []
class _Ctx:
def __enter__(self):
acquired.append("locked")
return self
def __exit__(self, *exc):
return False
monkeypatch.setattr(bot_relay, "acquire_turn_lock", lambda root, profile: _Ctx())
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
with bot_mode_dm._delivery_lock(
[str(tmp_path / "venv" / "bin" / "hermes"), "-p", "ops", "chat"],
stdin_file=False,
):
pass
with bot_mode_dm._delivery_lock(["hermes", "-p", "ops", "chat"], stdin_file=False):
pass
with bot_mode_dm._delivery_lock(
["C:\\venv\\Scripts\\hermes.exe", "-p", "ops", "chat"], stdin_file=False
):
pass
assert acquired == ["locked", "locked", "locked"]
# Unrelated argvs still bypass the lock entirely.
with bot_mode_dm._delivery_lock(["python", "-m", "whatever"], stdin_file=False):
pass
assert acquired == ["locked", "locked", "locked"]