Files
hermes-agent/tests/hermes_cli/test_shared_profile_warning.py
ethernet 26c4e8b160 refactor(update): use PM and shared source builders
PM owns Python dependency generations. Shared frontend builders own Node
preparation and compilation. Route source updates and launchers through
these owners instead of separate repair ladders.

Remove obsolete live-venv holder gates and soft build-failure plumbing.
Preserve source validation, staged publication, fleet outcomes, and
historical relaunch hooks.

Verification: 956 tests passed in the combined focused run, with 43 skips.
After the final ZIP exit fix, 583 focused tests passed. Shared JavaScript
builder tests, Ruff, and diff checks passed. Native Windows/macOS and full
packaged-app builds were not run.
2026-09-12 13:45:08 -04:00

226 lines
9.6 KiB
Python

"""Live subprocesses, real ledger I/O, and isolated profile homes."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from contextlib import contextmanager
from pathlib import Path
import pytest
from hermes_cli import process_identity
from hermes_constants import hermes_home_key
@pytest.fixture
def homes(tmp_path, monkeypatch):
root = tmp_path / "home"
root.mkdir()
monkeypatch.setenv("HERMES_HOME", str(root))
monkeypatch.setattr(Path, "home", lambda: tmp_path)
return root
@contextmanager
def running_install(home: Path, install: Path):
env = dict(os.environ, HERMES_HOME=str(home))
for key in ("HERMES_SPAWN", "HERMES_PARENT_PID", "HERMES_PARENT_START_MARKER"):
env.pop(key, None)
script = """
import sys
from pathlib import Path
from hermes_cli.process_identity import register_self
assert register_self('serve', project_root=Path(sys.argv[1]))
print('ready', flush=True)
sys.stdin.readline()
"""
child = subprocess.Popen(
[sys.executable, "-u", "-c", script, str(install)],
env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True,
)
try:
assert child.stdout.readline().strip() == "ready"
yield child
finally:
child.communicate("exit\n", timeout=15)
assert child.returncode == 0
def test_warning_tracks_live_other_install_in_same_home(homes, tmp_path):
from hermes_cli.shared_profile_warning import shared_profile_warning
current = tmp_path / "stable"
other = tmp_path / "canary"
assert shared_profile_warning(project_root=current) == ""
with running_install(homes, other) as child:
warning = shared_profile_warning(project_root=current)
assert warning and "profile" in warning.lower()
assert shared_profile_warning(project_root=other) == ""
entry = next(e for e in process_identity.ledger_entries(project_root=other) if e["pid"] == child.pid)
assert entry["hermes_home"] == hermes_home_key(homes)
assert shared_profile_warning(home=homes / "profiles" / "work", project_root=current) == ""
# A stale file is not proof of concurrent use.
assert process_identity._ledger_path().exists()
assert shared_profile_warning(project_root=current) == ""
work = homes / "profiles" / "work"
work.mkdir(parents=True)
with running_install(work, other):
assert shared_profile_warning(project_root=current) == ""
assert shared_profile_warning(home=work, project_root=current)
@contextmanager
def running_cli(home: Path, install: Path):
"""Run the real CLI constructor and entrypoint; stop at terminal interaction."""
env = dict(os.environ, HERMES_HOME=str(home), HOME=str(home.parent), USERPROFILE=str(home.parent))
for key in ("HERMES_SPAWN", "HERMES_PARENT_PID", "HERMES_PARENT_START_MARKER"):
env.pop(key, None)
script = """
import sys
from pathlib import Path
import hermes_constants
# Model two installed runtime roots without copying the source tree.
hermes_constants.PROJECT_ROOT = Path(sys.argv[1])
import cli
def terminal_boundary(self):
print('cli-ready', flush=True)
sys.stdin.readline()
cli.HermesCLI.run = terminal_boundary
cli.main(model='test-model', provider='openai', api_key='test-only',
base_url='http://127.0.0.1:1/v1', toolsets='none', ignore_rules=True)
"""
output = {}
child = subprocess.Popen(
[sys.executable, "-u", "-c", script, str(install)],
env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True,
)
try:
assert child.stdout is not None and child.stderr is not None
for line in child.stdout:
if line.strip() == "cli-ready":
break
else:
raise AssertionError(child.stderr.read())
yield child, output
finally:
_, output["stderr"] = child.communicate("exit\n", timeout=15)
assert child.returncode == 0, output["stderr"]
@pytest.mark.parametrize("other_surface", ["serve", "cli"])
def test_cli_entrypoint_registers_and_warns_once_for_live_shared_home(homes, tmp_path, monkeypatch, other_surface):
from hermes_cli.shared_profile_warning import shared_profile_warning
stable = tmp_path / "stable"
canary = tmp_path / "canary"
start_other = running_install if other_surface == "serve" else running_cli
with start_other(homes, stable):
with running_cli(homes, canary) as (child, output):
entries = process_identity.ledger_entries(project_root=canary, verified_only=True)
own = [entry for entry in entries if entry["pid"] == child.pid]
assert len(own) == 1
assert own[0]["purpose"] == "cli"
assert own[0]["hermes_home"] == hermes_home_key(homes)
assert shared_profile_warning(project_root=stable)
# A CLI record must not make a terminal process an update-owned backend.
import hermes_constants
from hermes_cli.update_inventory import UpdatePlan, _collect_ledger_runtimes
with monkeypatch.context() as patcher:
patcher.setattr(hermes_constants, "PROJECT_ROOT", canary, raising=False)
assert own == process_identity.ledger_entries(verified_only=True)
assert own[0]["purpose"] not in process_identity.REAPABLE_PURPOSES
plan = UpdatePlan()
_collect_ledger_runtimes(plan, set())
assert plan.runtimes == []
assert child.poll() is None
assert output["stderr"].count("Another Hermes installation") == 1
assert shared_profile_warning(project_root=stable) == ""
work = homes / "profiles" / "work"
work.mkdir(parents=True)
with running_install(homes, stable), running_cli(work, canary) as (_, output):
assert shared_profile_warning(project_root=stable) == ""
assert "Another Hermes installation" not in output["stderr"]
@pytest.mark.parametrize("corrupt", [b"{broken", b"\xff"])
def test_cli_startup_quarantines_corrupt_ledger(homes, tmp_path, corrupt):
ledger = process_identity._ledger_path()
ledger.write_bytes(corrupt)
install = tmp_path / "cli-install"
with running_cli(homes, install) as (child, _):
assert ledger.with_suffix(".json.corrupt").read_bytes() == corrupt
entries = process_identity.ledger_entries(project_root=install, verified_only=True)
assert [entry["pid"] for entry in entries] == [child.pid]
assert entries[0]["hermes_home"] == hermes_home_key(homes)
def test_status_surfaces_live_warning_without_host_details(homes, tmp_path, monkeypatch):
from fastapi.testclient import TestClient
from hermes_cli import web_server
monkeypatch.setattr(web_server.app.state, "auth_required", True, raising=False)
client = TestClient(web_server.app)
assert client.get("/api/status").json()["shared_profile_warning"] is False
with running_cli(homes, tmp_path / "canary"):
response = client.get("/api/status")
assert response.status_code == 200
assert response.json()["shared_profile_warning"] is True
assert client.get("/api/status").json()["shared_profile_warning"] is False
work = homes / "profiles" / "work"
work.mkdir(parents=True)
(work / "config.yaml").write_text("{}")
with running_install(work, tmp_path / "canary"):
assert client.get("/api/status").json()["shared_profile_warning"] is False
response = client.get("/api/status?profile=work")
assert response.status_code == 200
assert response.json()["shared_profile_warning"] is True
def test_warning_rejects_reused_or_unverifiable_process_identity(homes, tmp_path):
from hermes_cli.shared_profile_warning import shared_profile_warning
current = tmp_path / "stable"
with running_install(homes, tmp_path / "canary"):
ledger = process_identity._ledger_path()
entries = json.loads(ledger.read_text())
original = entries[0]["create_time"]
assert shared_profile_warning(project_root=current)
for bad_create in (original - 60, original - 0.5, None):
entries[0]["create_time"] = bad_create
ledger.write_text(json.dumps(entries))
assert shared_profile_warning(project_root=current) == ""
entries[0]["create_time"] = original
entries[0].pop("hermes_home", None)
ledger.write_text(json.dumps(entries))
assert shared_profile_warning(project_root=current) == ""
entries[0]["hermes_home"] = hermes_home_key(homes / "profiles" / "other")
ledger.write_text(json.dumps(entries))
assert shared_profile_warning(project_root=current) == ""
entries[0]["hermes_home"] = hermes_home_key(homes)
ledger.write_text(json.dumps(entries))
assert shared_profile_warning(project_root=current)
# Even a valid record is not live proof if the process probe is unavailable.
from unittest.mock import patch
with patch.object(process_identity, "_pid_alive_matches", return_value=None):
assert shared_profile_warning(project_root=current) == ""
assert shared_profile_warning(project_root=current)
ledger.write_text("{broken")
assert shared_profile_warning(project_root=current) == ""
assert ledger.with_suffix(".json.corrupt").exists()
ledger.write_text(json.dumps(entries))
assert shared_profile_warning(project_root=current)
ledger.unlink()
assert shared_profile_warning(project_root=current) == ""
assert not ledger.exists()
assert not (homes / "config.yaml").exists()
assert not (homes / "state.db").exists()