fix(#2698): keep the pinned home across isolated fallbacks

This commit is contained in:
Rod Boev
2026-06-19 20:03:16 -04:00
committed by nesquena-hermes
parent c7375c60c4
commit e77ca8dbac
4 changed files with 92 additions and 1 deletions
+5 -1
View File
@@ -507,6 +507,8 @@ def install_cron_scheduler_profile_isolation() -> None:
return original(job, *args, **kwargs)
finally:
event_profile = str((job or {}).get("profile") or "").strip() or None
if _is_isolated_profile_mode():
event_profile = _isolated_profile_name()
try:
publish_session_list_changed("cron_complete", profile=event_profile)
except TypeError:
@@ -1102,7 +1104,9 @@ def switch_profile(name: str, *, process_wide: bool = True) -> dict:
)
# Resolve profile directory
if _is_root_profile(name):
if _is_isolated_profile_mode():
home = Path(_INITIAL_HERMES_HOME).expanduser()
elif _is_root_profile(name):
home = _DEFAULT_HERMES_HOME
else:
home = _resolve_named_profile_home(name)
+2
View File
@@ -549,6 +549,8 @@ def main() -> int:
# for SO_EXCLUSIVEADDRUSE.
_CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
subprocess.Popen([python_exe, server_path],
cwd=server_cwd,
env=os.environ.copy(),
creationflags=_CREATE_NEW_PROCESS_GROUP)
sys.exit(0)
os.execv(python_exe, [python_exe, server_path])
+22
View File
@@ -429,6 +429,28 @@ class TestForegroundEnvAndCwd:
assert chdir_calls == [str(workspace)]
def test_windows_foreground_popen_uses_server_cwd_override(self, setup, monkeypatch, clean_env, tmp_path):
bs, _agent_dir = setup
workspace = tmp_path / "workspace-win"
workspace.mkdir()
monkeypatch.setenv("HERMES_WEBUI_SERVER_CWD", str(workspace))
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground"])
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(os, "chdir", lambda p: None)
popen_calls = []
def fake_popen(*args, **kwargs):
popen_calls.append((args, kwargs))
return None
monkeypatch.setattr(subprocess, "Popen", fake_popen)
with pytest.raises(SystemExit):
bs.main()
assert popen_calls[0][1]["cwd"] == str(workspace)
def test_foreground_exports_resolved_env_vars(self, setup, monkeypatch, clean_env):
bs, agent_dir = setup
monkeypatch.setattr(sys, "argv", [
@@ -10,6 +10,7 @@ import os
import io
import sys
import tempfile
import types
from pathlib import Path
from unittest import mock
@@ -421,6 +422,28 @@ class TestProfileMutationsInIsolatedMode:
except (ImportError, ValueError, RuntimeError):
pass # expected in test env without hermes_cli
def test_switch_to_same_default_profile_keeps_pinned_home(self, temp_hermes_home, monkeypatch, tmp_path):
"""A same-name switch for isolated profiles/default must keep using the pinned home."""
isolated_default = temp_hermes_home / "profiles" / "default"
isolated_default.mkdir(parents=True)
(isolated_default / "workspace").mkdir()
base_workspace = tmp_path / "base-workspace"
isolated_workspace = tmp_path / "isolated-workspace"
base_workspace.mkdir()
isolated_workspace.mkdir()
(temp_hermes_home / "config.yaml").write_text(f"workspace: {base_workspace}\n", encoding="utf-8")
(isolated_default / "config.yaml").write_text(f"workspace: {isolated_workspace}\n", encoding="utf-8")
monkeypatch.setenv("HERMES_HOME", str(isolated_default))
monkeypatch.setenv("HERMES_BASE_HOME", "")
monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", temp_hermes_home)
monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(isolated_default))
monkeypatch.setattr(_profiles_mod, "list_profiles_api", lambda: [])
result = switch_profile("default", process_wide=False)
assert result["default_workspace"] == str(isolated_workspace.resolve())
def test_scheduled_cron_jobs_stay_pinned_to_isolated_home(self, temp_single_profile, monkeypatch):
"""Scheduler jobs must not resolve foreign profile homes in isolated mode."""
base_home = temp_single_profile.parent.parent
@@ -467,6 +490,46 @@ class TestProfileMutationsInIsolatedMode:
403,
)
def test_scheduler_publishes_isolated_profile_after_foreign_job_profile(self, temp_single_profile, monkeypatch):
"""Scheduled cron completion must publish the isolated profile identity."""
events = []
base_home = temp_single_profile.parent.parent
cron_pkg = types.ModuleType("cron")
cron_pkg.__path__ = []
cron_scheduler = types.ModuleType("cron.scheduler")
cron_scheduler.run_job = lambda job: events.append(("run", job["id"])) or "ok"
class _Ctx:
def __init__(self, home):
self.home = str(home)
def __enter__(self):
events.append(("enter", self.home))
return self
def __exit__(self, exc_type, exc, tb):
events.append(("exit", self.home))
return False
monkeypatch.setitem(sys.modules, "cron", cron_pkg)
monkeypatch.setitem(sys.modules, "cron.scheduler", cron_scheduler)
monkeypatch.setenv("HERMES_HOME", str(temp_single_profile))
monkeypatch.setenv("HERMES_BASE_HOME", "")
monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", base_home)
monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(temp_single_profile))
monkeypatch.setattr(_profiles_mod, "cron_profile_context_for_home", _Ctx)
monkeypatch.setattr(
_profiles_mod,
"publish_session_list_changed",
lambda reason, profile=None: events.append(("publish", reason, profile)),
)
_profiles_mod.install_cron_scheduler_profile_isolation()
assert cron_scheduler.run_job({"id": "job2698", "profile": "other"}) == "ok"
assert events[-1] == ("publish", "cron_complete", "user1")
class TestNormalModePreservation:
"""Test that normal mode behavior is completely unchanged."""