mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-24 10:40:16 +00:00
96 lines
3.3 KiB
Python
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
|