Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion tests/tools/test_bot_mode_dm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import pytest

from tools import bot_mode_dm, bot_mode_probe
from tools import bot_mode_dm, bot_mode_probe, bot_relay


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -234,6 +234,9 @@ def _runner_author(command):

def test_local_delivery_command_and_ack(tmp_path, monkeypatch):
calls = _capture_spawn(monkeypatch)
# These assertions target the -p/turn-args shape; pin the entrypoint resolution
# so the test stays hermetic across venvs that do/don't expose a sibling script.
monkeypatch.setattr(bot_relay, "_hermes_cli", lambda: "hermes")
home = _managed_home(tmp_path, teammates=("researcher",))
agent = _FakeAgent(home, title="Bot Chat")

Expand Down Expand Up @@ -295,6 +298,7 @@ def test_peer_delivery_command_pins_registry_profile_for_secondary_bots(
tool-side roster (read from the machine-root config) validated the
target."""
calls = _capture_spawn(monkeypatch)
monkeypatch.setattr(bot_relay, "_hermes_cli", lambda: "hermes")
home = _managed_home(tmp_path, peers=("spark",))
# A reviewer-profile gateway context: the agent's session db lives under
# that profile's home, so _agent_home() resolves there while the
Expand All @@ -316,6 +320,7 @@ def test_peer_delivery_command_pins_registry_profile_for_secondary_bots(

def test_peer_delivery_command(tmp_path, monkeypatch):
calls = _capture_spawn(monkeypatch)
monkeypatch.setattr(bot_relay, "_hermes_cli", lambda: "hermes")
monkeypatch.setattr("socket.gethostname", lambda: "eri-mac.local")
home = _managed_home(tmp_path, peers=("spark",))
agent = _FakeAgent(home, title="Bot Chat")
Expand All @@ -341,6 +346,40 @@ def test_peer_delivery_command(tmp_path, monkeypatch):
assert transport_argv == ["hermes", "-p", "default", "peer", "dm", "spark"]


def test_delivery_pins_the_hermes_entrypoint_beside_this_interpreter(tmp_path, monkeypatch):
"""A background delivery must not rely on PATH: the runner's service context
lacks the gateway's venv bin dir, so a bare ``hermes`` resolves to a system
install whose shebang picks the wrong interpreter and dies on import (#108628).
Both transports must invoke the entrypoint beside this interpreter instead."""
venv_bin = tmp_path / "venv" / ("Scripts" if sys.platform == "win32" else "bin")
venv_bin.mkdir(parents=True)
hermes_entry = venv_bin / ("hermes.exe" if sys.platform == "win32" else "hermes")
hermes_entry.write_text("#!/bin/sh\n", encoding="utf-8")
monkeypatch.setattr(sys, "executable", str(venv_bin / "python3"))

calls = _capture_spawn(monkeypatch)
home = _managed_home(tmp_path, teammates=("researcher",), peers=("spark",))
agent = _FakeAgent(home, title="Bot Chat")

result = json.loads(
bot_mode_dm.message_agent_tool(target="researcher", message="ping", agent=agent)
)
assert result["status"] == "sent"
mode, _dm_file, transport_argv = _runner_parts(calls[0]["command"])
assert mode == "query-file"
assert transport_argv[0] == str(hermes_entry)
assert transport_argv[1:] == ["-p", "researcher", "chat", "--in", "~", "-c", "Bot Chat",
"--create-if-missing", "-Q"]

result2 = json.loads(
bot_mode_dm.message_agent_tool(target="spark", message="ping", agent=agent)
)
assert result2["status"] == "sent"
mode, _dm_file, transport_argv = _runner_parts(calls[1]["command"])
assert mode == "stdin"
assert transport_argv == [str(hermes_entry), "-p", "default", "peer", "dm", "spark"]


def test_peer_delivery_author_carries_the_sender_hostname_and_local_stays_bare(tmp_path, monkeypatch):
"""A peer dm crosses installs, so its author id is ``bot:<hostname>/<profile>``: the peer's own ``coder`` and a
remote ``coder`` must not share one id. A teammate on this install still sees the bare ``bot:coder``."""
Expand Down
11 changes: 8 additions & 3 deletions tools/bot_mode_dm.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def message_agent_tool(target: str = "", message: str = "", task_id: Optional[st
BOT_CHAT_TITLE, _handle, _hermes_root, _peers, _profile_name as _self_profile_name, _roster,
is_bot_mode_managed,
)
from tools.bot_relay import BOT_CHAT_TURN_ARGS
from tools.bot_relay import BOT_CHAT_TURN_ARGS, _hermes_cli

if _session_title(agent) != BOT_CHAT_TITLE:
return _err("message_agent is only available in a Bot Mode 'Bot Chat' session. "
Expand Down Expand Up @@ -230,7 +230,12 @@ def _roster_err(msg: str) -> str:
# Pin the registry-owning profile: `hermes peer` resolves bot_peers via the profile-scoped
# load_config(), while the roster above reads the machine-root config — the CLI must run
# in that same profile or a secondary-profile bot sees an empty registry.
return _start_delivery(["hermes", "-p", _self_profile_name(root), "peer", "dm", dm_target], content,
# The delivery runs in a background service context whose PATH lacks the gateway's
# venv bin dir, so a bare "hermes" resolves to a system install and dies on import
# under the wrong interpreter (#108628). _hermes_cli pins the entrypoint beside
# this interpreter; _delivery_lock/_local_delivery_home match argv[0] by basename,
# so the absolute path stays compatible.
return _start_delivery([_hermes_cli(), "-p", _self_profile_name(root), "peer", "dm", dm_target], content,
f"@{peer_profile or peer_name} on peer '{peer_name}'", stdin_file=True,
author=peer_author, **delivery)

Expand All @@ -251,7 +256,7 @@ def _roster_err(msg: str) -> str:
return _roster_err(f"No teammate named '{raw_target}' on this install, on a connected "
"machine, or on a registered peer. Pick a name from the roster "
"(roles are listed in your system prompt).")
return _start_delivery(["hermes", "-p", resolved, *BOT_CHAT_TURN_ARGS], content, f"@{_handle(resolved)}",
return _start_delivery([_hermes_cli(), "-p", resolved, *BOT_CHAT_TURN_ARGS], content, f"@{_handle(resolved)}",
stdin_file=False, profile_home=roster_homes[resolved], author=author, **delivery)


Expand Down