Files
hermes-webui/tests/test_issue1700_parallel_profile_switch.py
2026-05-06 16:11:46 +00:00

96 lines
3.3 KiB
Python

"""Regression coverage for issue #1700 parallel profile switching.
A WebUI profile switch uses cookie/thread-local profile state, so it should be
allowed while another session is streaming. Only process-wide profile switches
must remain blocked because they mutate global Hermes runtime state.
"""
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent.resolve()
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
def _extract_switch_to_profile() -> str:
marker = "async function switchToProfile(name) {"
idx = PANELS_JS.find(marker)
assert idx != -1, "switchToProfile() not found in static/panels.js"
depth = 0
for i, ch in enumerate(PANELS_JS[idx:], idx):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return PANELS_JS[idx : i + 1]
raise AssertionError("Could not extract switchToProfile() body")
def _prepare_profile_tree(tmp_path, monkeypatch):
import api.profiles as profiles
default_home = tmp_path / ".hermes"
target_home = default_home / "profiles" / "writer"
target_workspace = tmp_path / "writer-workspace"
target_workspace.mkdir(parents=True)
target_home.mkdir(parents=True)
(target_home / "config.yaml").write_text(
f"model:\n provider: openai-codex\n default: gpt-5.5\n"
f"terminal:\n cwd: {target_workspace}\n",
encoding="utf-8",
)
monkeypatch.setattr(profiles, "_DEFAULT_HERMES_HOME", default_home)
monkeypatch.setattr(profiles, "_active_profile", "default")
monkeypatch.setattr(profiles, "list_profiles_api", lambda: [{"name": "default"}, {"name": "writer"}])
profiles._tls.profile = None
return profiles
def test_process_wide_switch_still_blocks_when_stream_is_active(tmp_path, monkeypatch):
profiles = _prepare_profile_tree(tmp_path, monkeypatch)
from api.config import STREAMS
STREAMS.clear()
STREAMS["stream-default"] = object()
try:
with pytest.raises(RuntimeError, match="Cannot switch profiles while an agent is running"):
profiles.switch_profile("writer", process_wide=True)
finally:
STREAMS.clear()
profiles._tls.profile = None
def test_per_client_switch_allowed_when_stream_is_active(tmp_path, monkeypatch):
profiles = _prepare_profile_tree(tmp_path, monkeypatch)
from api.config import STREAMS
STREAMS.clear()
STREAMS["stream-default"] = object()
try:
result = profiles.switch_profile("writer", process_wide=False)
finally:
STREAMS.clear()
profiles._tls.profile = None
assert result["active"] == "writer"
assert result["default_model"] == "gpt-5.5"
def test_frontend_profile_switch_no_longer_blocks_on_busy_state():
fn = _extract_switch_to_profile()
assert "profiles_busy_switch" not in fn
assert "if (S.busy)" not in fn
assert "Profile switches are per-client cookie/TLS scoped" in fn
def test_frontend_treats_active_or_pending_session_as_in_progress():
fn = _extract_switch_to_profile()
session_block = fn[fn.find("const sessionInProgress") : fn.find("try {", fn.find("const sessionInProgress"))]
assert "S.session.active_stream_id" in session_block
assert "S.session.pending_user_message" in session_block
assert "S.messages.length > 0" in session_block