"""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()