Narrows the previous commit to the one case where the outcome is really unknown. When the sender has already pinned a live intent (<dm>.live.json) it may have admitted the DM and told the model not to resend; a runner that then fails hermes_bootstrap now prints the same ambiguous payload (same delivery id, "Do not resend") that _run_delivery prints for an admission failure. Without an intent nothing was handed over, so the plain repair hint stays the truth and a resend remains correct. The payload is shared with _run_delivery via _live_outcome_unknown instead of a second classifier, and the evidence-probing variants and the separate test module are dropped in favour of one real -I -S subprocess test plus an explicit negative assertion on the existing no-intent test.
156 lines
7.3 KiB
Python
156 lines
7.3 KiB
Python
"""The ``bot_mode_dm.py`` runner entry boots Hermes' dependency environment itself.
|
|
|
|
``message_agent`` spawns ``<sys.executable> tools/bot_mode_dm.py --run-delivery …`` and the relay
|
|
spawns ``… --wait-reply …`` (``bot_relay.waiter_command``). Under a PM-managed install
|
|
``sys.executable`` is the bare store interpreter: dependencies are activated in-process at boot,
|
|
and the terminal backend strips the Hermes-owned ``PYTHONPATH``, so a runner that does not boot
|
|
like every other entry point dies on its first third-party import. Live, every local Bot Chat DM
|
|
came back ``Live admission outcome unknown: No module named 'ruamel'. Do not resend.``
|
|
|
|
Each test launches the real script under ``-I -S`` (no site-packages, no ``PYTHON*`` env), with a
|
|
PM-committed dependency record in a temp ``HERMES_HOME`` as the only way to reach the packages.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
|
from tools import bot_relay
|
|
from tools.bot_mode_dm import _dm_delivery_id
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
RUNNER = REPO / "tools" / "bot_mode_dm.py"
|
|
# A bare interpreter: isolated (-I ignores PYTHONPATH/PYTHONHOME, user site) and no site
|
|
# module (-S), so no third-party package is importable unless the entry activates one.
|
|
BARE = [sys.executable, "-I", "-S"]
|
|
|
|
|
|
@pytest.fixture
|
|
def committed_home(tmp_path, monkeypatch):
|
|
"""A temp ``HERMES_HOME`` whose install state commits a dependency generation for REPO.
|
|
|
|
The generation's site-packages carries a ``.pth`` naming this interpreter's own package
|
|
directories, so activating it (and only activating it) makes the real dependencies importable.
|
|
"""
|
|
home = tmp_path / "hermes-home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
venv = install_state_dir(REPO) / "environments" / "gen" / "venv"
|
|
venv.mkdir(parents=True)
|
|
(venv / "pyvenv.cfg").write_text(
|
|
"version = {}.{}.{}\n".format(*sys.version_info[:3]), encoding="utf-8")
|
|
packages = site_packages(venv)
|
|
packages.mkdir(parents=True)
|
|
dirs = [p for p in sys.path if p and Path(p).is_dir() and Path(p).resolve() != REPO]
|
|
(packages / "hermes-test-deps.pth").write_text("\n".join(dirs) + "\n", encoding="utf-8")
|
|
return home, _commit(home, venv)
|
|
|
|
|
|
@pytest.fixture
|
|
def uncommittable_home(tmp_path, monkeypatch):
|
|
"""A temp ``HERMES_HOME`` whose committed generation is gone, so activation cannot succeed."""
|
|
home = tmp_path / "hermes-home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
install_state_dir(REPO).mkdir(parents=True)
|
|
return home, _commit(home, install_state_dir(REPO) / "environments" / "missing" / "venv")
|
|
|
|
|
|
def _commit(home, venv):
|
|
runtime_facts_path(REPO).write_text(
|
|
json.dumps({"packages": {"venv": {"environment": str(venv)}}}), encoding="utf-8")
|
|
env = {k: v for k, v in os.environ.items() if k not in ("PYTHONPATH", "PYTHONHOME", "VIRTUAL_ENV")}
|
|
env["HERMES_HOME"] = str(home)
|
|
return env
|
|
|
|
|
|
def _run(argv, env):
|
|
return subprocess.run(argv, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
env=env, cwd=str(REPO.parent), timeout=120)
|
|
|
|
|
|
def test_delivery_runner_admits_through_dependencies_it_activates(tmp_path, committed_home):
|
|
"""Live admission (``_admit_live_dm``) imports Hermes' third-party graph. Launched bare, the
|
|
runner must activate the committed environment, find no live owner, and hand the DM to the
|
|
transport, which consumes the file — never report an ``ambiguous`` import failure."""
|
|
_home, env = committed_home
|
|
profile_home = tmp_path / "profile"
|
|
profile_home.mkdir()
|
|
dm_file = tmp_path / "dm.txt"
|
|
dm_file.write_text("hello teammate", encoding="utf-8")
|
|
# The transport echoes the DM file it receives via --query-file.
|
|
transport = [sys.executable, "-I", "-S", "-c", "import sys; print(open(sys.argv[2], encoding='utf-8').read())"]
|
|
|
|
result = _run([*BARE, str(RUNNER), "--run-delivery", "query-file", str(dm_file),
|
|
"--profile-home", str(profile_home), *transport], env)
|
|
|
|
assert "No module named" not in result.stdout + result.stderr, result
|
|
assert result.returncode == 0, result
|
|
assert result.stdout.strip() == "hello teammate"
|
|
assert not dm_file.exists()
|
|
|
|
|
|
def test_delivery_runner_that_cannot_activate_names_the_repair_remedy(tmp_path, uncommittable_home):
|
|
"""When the committed environment cannot be activated, the runner exits with PM's
|
|
``hermes pm repair`` remedy before touching the DM, instead of an ambiguous
|
|
``No module named …`` outcome the sender is told not to resend."""
|
|
_home, env = uncommittable_home
|
|
dm_file = tmp_path / "dm.txt"
|
|
dm_file.write_text("hello teammate", encoding="utf-8")
|
|
|
|
result = _run([*BARE, str(RUNNER), "--run-delivery", "query-file", str(dm_file),
|
|
sys.executable, "-c", "pass"], env)
|
|
|
|
assert result.returncode == 1, result
|
|
assert "hermes pm repair" in result.stderr
|
|
assert "No module named" not in result.stdout + result.stderr
|
|
assert result.stdout == "" # nothing was handed over, so no "do not resend" outcome
|
|
assert dm_file.read_text(encoding="utf-8") == "hello teammate"
|
|
|
|
|
|
def test_delivery_runner_that_cannot_activate_after_live_admission_stays_ambiguous(tmp_path, uncommittable_home):
|
|
"""The sender may already have admitted this DM to the live owner (its pinned intent exists)
|
|
and was told not to resend. A runner that then cannot activate must keep that outcome
|
|
unknown, with the same delivery id, instead of a bare repair hint that reads as a failure."""
|
|
_home, env = uncommittable_home
|
|
dm_file = tmp_path / "dm.txt"
|
|
dm_file.write_text("hello teammate", encoding="utf-8")
|
|
intent = Path(str(dm_file) + ".live.json")
|
|
intent.write_text("{}", encoding="utf-8")
|
|
|
|
result = _run([*BARE, str(RUNNER), "--run-delivery", "query-file", str(dm_file),
|
|
"--profile-home", str(tmp_path), sys.executable, "-c", "pass"], env)
|
|
|
|
assert result.returncode == 1, result
|
|
assert "hermes pm repair" in result.stderr
|
|
payload = json.loads(result.stdout)
|
|
assert payload["status"] == "ambiguous"
|
|
assert payload["delivery_id"] == _dm_delivery_id(str(dm_file))
|
|
assert "Do not resend" in payload["error"]
|
|
assert dm_file.exists() and intent.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("home", ["committed_home", "uncommittable_home"])
|
|
def test_reply_waiter_prints_the_reply_whatever_the_dependency_state(tmp_path, request, home):
|
|
"""The relay's reply waiter shares the entry but is stdlib only, and its stdout is the
|
|
sender's wake-up: its argv, exactly as ``waiter_command`` builds it, must print the reply
|
|
whether or not the committed environment can be activated."""
|
|
_home, env = request.getfixturevalue(home)
|
|
root = tmp_path / "root"
|
|
envelope = {"id": "e" * 32, "target_handle": "researcher", "target_connection": "ssh-vps"}
|
|
reply_path = bot_relay.relay_root(root) / bot_relay.REPLIES_DIR / f"{envelope['id']}.json"
|
|
reply_path.parent.mkdir(parents=True)
|
|
reply_path.write_text(json.dumps({"reply": "pong"}), encoding="utf-8")
|
|
argv = shlex.split(bot_relay.waiter_command(root, envelope))
|
|
|
|
result = _run([*BARE, *argv[1:]], env)
|
|
|
|
assert result.returncode == 0, result
|
|
assert result.stdout.splitlines() == ["Reply from @researcher on ssh-vps:", "pong"]
|