Files
hermes-webui/tests/test_issue4775_sidebar_hidden_zero_message_pushdown.py
T

350 lines
11 KiB
Python

"""Regression coverage for issue #4775: hide zero-message and default_hidden rows for default sidebar loads."""
import io
import json
from pathlib import Path
import shutil
import subprocess
from urllib.parse import urlparse
import api.profiles as profiles
import api.routes as routes
import pytest
ROOT = Path(__file__).resolve().parents[1]
SESSIONS_JS = ROOT / "static" / "sessions.js"
NODE = shutil.which("node")
class _FakeHandler:
def __init__(self):
self.status = None
self.headers = {}
self.wfile = io.BytesIO()
def send_response(self, status):
self.status = status
def send_header(self, key, value):
self.headers[key] = value
def end_headers(self):
pass
def json_body(self):
return json.loads(self.wfile.getvalue().decode("utf-8"))
def _session_rows():
return [
{
"session_id": "visible-with-message",
"title": "Visible message",
"profile": "default",
"archived": False,
"message_count": 3,
"updated_at": 1000,
"last_message_at": 1000,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": False,
},
{
"session_id": "hidden-by-default",
"title": "Default hidden",
"profile": "default",
"archived": False,
"message_count": 1,
"updated_at": 900,
"last_message_at": 900,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": True,
},
{
"session_id": "zero-message",
"title": "Plain zero message",
"profile": "default",
"archived": False,
"message_count": 0,
"updated_at": 800,
"last_message_at": 800,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": False,
},
{
"session_id": "active-stream-row",
"title": "Streaming row",
"profile": "default",
"archived": False,
"message_count": 0,
"active_stream_id": "stream-active",
"updated_at": 700,
"last_message_at": 700,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": False,
},
{
"session_id": "attention-row",
"title": "Attention row",
"profile": "default",
"archived": False,
"message_count": 0,
"updated_at": 600,
"last_message_at": 600,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": False,
},
{
"session_id": "pending-user-row",
"title": "Pending user row",
"profile": "default",
"archived": False,
"message_count": 0,
"pending_user_message": "user typing",
"updated_at": 500,
"last_message_at": 500,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": False,
},
{
"session_id": "pending-flag-row",
"title": "Pending flag row",
"profile": "default",
"archived": False,
"message_count": 0,
"has_pending_user_message": True,
"updated_at": 550,
"last_message_at": 550,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
"default_hidden": False,
},
]
def _handle_sessions(url):
handler = _FakeHandler()
routes.handle_get(handler, urlparse(url))
return handler
def _extract_function(source_text, function_name):
marker = f"function {function_name}("
start = source_text.index(marker)
brace_start = source_text.index("{", start)
depth = 0
for index in range(brace_start, len(source_text)):
char = source_text[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source_text[start : index + 1]
raise AssertionError(f"Could not extract {function_name}")
def _run_node(script):
proc = subprocess.run([NODE, "-e", script], capture_output=True, text=True, check=True)
return json.loads(proc.stdout)
@pytest.fixture(autouse=True)
def _clear_cache():
routes._session_list_cache_clear()
yield
routes._session_list_cache_clear()
def _install_common_monkeypatches(monkeypatch, rows):
session_ids = {str(row["session_id"]) for row in rows if row.get("session_id")}
monkeypatch.setattr(routes, "all_sessions", lambda diag=None: list(rows))
monkeypatch.setattr(routes, "_reconcile_stale_stream_state_for_session_rows", lambda _rows: False)
monkeypatch.setattr(routes, "_enrich_sidebar_lineage_metadata", lambda _rows: None)
monkeypatch.setattr(
routes,
"_session_attention_summary",
lambda session_id: {"kind": "clarify", "count": 1}
if str(session_id) == "attention-row"
else None,
)
monkeypatch.setattr(routes, "get_cli_sessions", lambda source_filter=None, all_profiles=False: [])
monkeypatch.setattr(routes, "agent_session_rows_existing", lambda ids, profile=None: set(ids) & session_ids)
monkeypatch.setattr(routes, "load_settings", lambda: {"show_cli_sessions": True})
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: "default")
return rows
def test_default_sidebar_excludes_hidden_and_plain_zero_message_rows(monkeypatch):
rows = _session_rows()
_install_common_monkeypatches(monkeypatch, rows)
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui&exclude_hidden=1")
body = handler.json_body()
assert handler.status == 200
assert [r["session_id"] for r in body["sessions"]] == [
"visible-with-message",
"active-stream-row",
"attention-row",
"pending-flag-row",
"pending-user-row",
]
def test_zero_message_rows_with_visibility_signals_survive(monkeypatch):
rows = _session_rows()
_install_common_monkeypatches(monkeypatch, rows)
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui&exclude_hidden=1")
body = handler.json_body()
by_id = {row["session_id"]: row for row in body["sessions"]}
assert handler.status == 200
assert set(by_id) >= {
"active-stream-row",
"attention-row",
"pending-flag-row",
"pending-user-row",
}
assert by_id["active-stream-row"]["active_stream_id"] == "stream-active"
assert by_id["attention-row"]["attention"]["kind"] == "clarify"
assert by_id["pending-flag-row"]["has_pending_user_message"] is True
assert by_id["pending-user-row"]["title"] == "Pending user row"
def test_omit_exclude_hidden_still_returns_default_hidden_rows(monkeypatch):
rows = _session_rows()
_install_common_monkeypatches(monkeypatch, rows)
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui")
body = handler.json_body()
assert handler.status == 200
session_ids = {row["session_id"] for row in body["sessions"]}
assert "hidden-by-default" in session_ids
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_default_and_unassigned_queries_send_exclude_hidden(monkeypatch):
src = SESSIONS_JS.read_text(encoding="utf-8")
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
project_filter_fn = _extract_function(src, "_setActiveProjectFilter")
query_fn = _extract_function(src, "_sessionListQueryString")
script = f"""
global.window = {{ _showCliSessions: false }};
global._showCliSessions = false;
global._showAllProfiles = false;
global._showArchived = false;
global._activeProject = null;
global.NO_PROJECT_FILTER = '__none__';
global.renderSessionListFromCache = () => {{}};
global.renderSessionList = () => Promise.resolve();
{requested_source_fn}
{exclude_hidden_fn}
{project_filter_fn}
{query_fn}
const default_query = _sessionListQueryString();
_setActiveProjectFilter('__none__');
const unassigned_query = _sessionListQueryString();
_setActiveProjectFilter('demo-project');
const named_project_query = _sessionListQueryString();
console.log(JSON.stringify({{ default_query, unassigned_query, named_project_query }}));
"""
body = _run_node(script)
assert body["default_query"] == "?sidebar_source=webui&exclude_hidden=1"
assert body["unassigned_query"] == "?sidebar_source=webui&exclude_hidden=1"
assert body["named_project_query"] == "?sidebar_source=webui"
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_project_filter_click_path_triggers_fresh_session_load():
src = SESSIONS_JS.read_text(encoding="utf-8")
project_filter_fn = _extract_function(src, "_setActiveProjectFilter")
script = f"""
const calls = [];
global.NO_PROJECT_FILTER = '__none__';
global._activeProject = null;
global.renderSessionListFromCache = () => {{
calls.push('cache');
}};
global.renderSessionList = (opts) => {{
calls.push(opts);
return Promise.resolve();
}};
{project_filter_fn}
_setActiveProjectFilter('demo-project');
_setActiveProjectFilter('__none__');
console.log(JSON.stringify({{
activeProject: global._activeProject,
calls,
}}));
"""
body = _run_node(script)
assert body["activeProject"] == "__none__"
assert body["calls"] == [
"cache",
{"deferWhileInteracting": False},
"cache",
{"deferWhileInteracting": False},
]
def test_cache_key_varies_for_exclude_hidden_and_visible_only():
key_without_filters = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=True,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
include_archived=False,
sidebar_source="webui",
)
key_exclude_hidden = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=True,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
include_archived=False,
exclude_hidden=True,
sidebar_source="webui",
)
key_visible_only = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=True,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
include_archived=False,
visible_only=True,
sidebar_source="webui",
)
assert key_without_filters != key_exclude_hidden
assert key_without_filters != key_visible_only
assert key_exclude_hidden != key_visible_only