788 lines
29 KiB
Python
788 lines
29 KiB
Python
"""Regression coverage for issue #4766: `/api/sessions` filters by active sidebar source."""
|
|
|
|
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(
|
|
webui_count,
|
|
cli_count,
|
|
archived_webui_count=0,
|
|
archived_cli_count=0,
|
|
start=0,
|
|
):
|
|
rows = []
|
|
for index in range(webui_count):
|
|
rows.append(
|
|
{
|
|
"session_id": f"webui-{start + index}",
|
|
"title": "WebUI Session",
|
|
"profile": "default",
|
|
"archived": index < archived_webui_count,
|
|
"message_count": 1,
|
|
"updated_at": 1000 + index,
|
|
"last_message_at": 1000 + index,
|
|
"source": "webui",
|
|
"raw_source": "webui",
|
|
"session_source": "webui",
|
|
"source_tag": "webui",
|
|
}
|
|
)
|
|
for index in range(cli_count):
|
|
rows.append(
|
|
{
|
|
"session_id": f"cli-{start + index + 10000}",
|
|
"title": "Imported CLI session",
|
|
"profile": "default",
|
|
"archived": index < archived_cli_count,
|
|
"message_count": 1,
|
|
"updated_at": 2000 + index,
|
|
"last_message_at": 2000 + index,
|
|
"source": "cli",
|
|
"raw_source": "cli",
|
|
"session_source": "cli",
|
|
"source_tag": "cli",
|
|
}
|
|
)
|
|
return rows
|
|
|
|
|
|
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 _ensure_async(function_source, function_name):
|
|
if function_source.startswith("async function "):
|
|
return function_source
|
|
return function_source.replace(
|
|
f"function {function_name}",
|
|
f"async function {function_name}",
|
|
1,
|
|
)
|
|
|
|
|
|
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):
|
|
enriched = []
|
|
row_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: enriched.append([r["session_id"] for r in rows]))
|
|
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(row_ids & {str(sid) for sid in ids}))
|
|
monkeypatch.setattr(routes, "load_settings", lambda: {"show_cli_sessions": True})
|
|
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: "default")
|
|
return enriched
|
|
|
|
|
|
def test_sidebar_source_webui_excludes_cli_rows(monkeypatch):
|
|
rows = _session_rows(webui_count=30, cli_count=20)
|
|
enriched = _install_common_monkeypatches(monkeypatch, rows)
|
|
|
|
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui")
|
|
|
|
body = handler.json_body()
|
|
assert handler.status == 200
|
|
assert len(body["sessions"]) == 30
|
|
assert all(r["session_id"].startswith("webui-") for r in body["sessions"])
|
|
assert body["webui_session_count"] == 30
|
|
assert body["cli_session_count"] == 20
|
|
assert body["archived_count"] == 0
|
|
expected = {
|
|
row["session_id"] for row in rows
|
|
if not row["archived"] and row["session_id"].startswith("webui-")
|
|
}
|
|
assert set(enriched[0]) == expected
|
|
|
|
|
|
def test_sidebar_source_webui_includes_hidden_archived_parent_reference(monkeypatch):
|
|
rows = [
|
|
{
|
|
"session_id": "webui-parent",
|
|
"title": "Archived parent",
|
|
"profile": "default",
|
|
"archived": True,
|
|
"message_count": 10,
|
|
"updated_at": 1000,
|
|
"last_message_at": 1000,
|
|
"source": "webui",
|
|
"raw_source": "webui",
|
|
"session_source": "webui",
|
|
"source_tag": "webui",
|
|
},
|
|
{
|
|
"session_id": "webui-child",
|
|
"title": "Subagent Session",
|
|
"profile": "default",
|
|
"archived": False,
|
|
"message_count": 4,
|
|
"updated_at": 1100,
|
|
"last_message_at": 1100,
|
|
"source": "subagent",
|
|
"raw_source": "subagent",
|
|
"session_source": "other",
|
|
"source_tag": "subagent",
|
|
"parent_session_id": "webui-parent",
|
|
"relationship_type": "child_session",
|
|
},
|
|
]
|
|
_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 [row["session_id"] for row in body["sessions"]] == ["webui-child"]
|
|
assert [row["session_id"] for row in body["sidebar_reference_sessions"]] == ["webui-parent"]
|
|
assert body["sidebar_reference_sessions"][0]["archived"] is True
|
|
assert body["sidebar_reference_sessions"][0]["_sidebar_reference_only"] is True
|
|
|
|
|
|
def test_sidebar_source_cli_excludes_webui_rows(monkeypatch):
|
|
rows = _session_rows(webui_count=30, cli_count=20)
|
|
_install_common_monkeypatches(monkeypatch, rows)
|
|
|
|
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=cli")
|
|
|
|
body = handler.json_body()
|
|
assert handler.status == 200
|
|
assert len(body["sessions"]) == 20
|
|
assert all(r["session_id"].startswith("cli-") for r in body["sessions"])
|
|
assert body["webui_session_count"] == 30
|
|
assert body["cli_session_count"] == 20
|
|
|
|
|
|
def test_sidebar_source_omitted_returns_all_rows(monkeypatch):
|
|
rows = _session_rows(webui_count=30, cli_count=20)
|
|
_install_common_monkeypatches(monkeypatch, rows)
|
|
|
|
handler = _handle_sessions("http://example.com/api/sessions")
|
|
|
|
body = handler.json_body()
|
|
assert handler.status == 200
|
|
assert len(body["sessions"]) == 50
|
|
assert len([r for r in body["sessions"] if r["session_id"].startswith("webui-")]) == 30
|
|
assert len([r for r in body["sessions"] if r["session_id"].startswith("cli-")]) == 20
|
|
|
|
|
|
def test_sidebar_source_returns_cross_bucket_counts(monkeypatch):
|
|
rows = _session_rows(webui_count=30, cli_count=20, archived_webui_count=2, archived_cli_count=3)
|
|
_install_common_monkeypatches(monkeypatch, rows)
|
|
|
|
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui&include_archived=1")
|
|
webui_rows = [r for r in rows if r["session_id"].startswith("webui-")]
|
|
cli_rows = [r for r in rows if r["session_id"].startswith("cli-")]
|
|
|
|
body = handler.json_body()
|
|
assert handler.status == 200
|
|
assert body["webui_session_count"] == len(webui_rows)
|
|
assert body["cli_session_count"] == len(cli_rows)
|
|
|
|
|
|
def test_sidebar_source_preserves_archived_counts(monkeypatch):
|
|
rows = _session_rows(webui_count=30, cli_count=20, archived_webui_count=2, archived_cli_count=3)
|
|
_install_common_monkeypatches(monkeypatch, rows)
|
|
|
|
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui&include_archived=1")
|
|
body = handler.json_body()
|
|
|
|
assert handler.status == 200
|
|
assert body["archived_webui_count"] == 2
|
|
assert body["archived_cli_count"] == 3
|
|
assert body["archived_count"] == 5
|
|
assert len([r for r in body["sessions"] if r["archived"]]) == 2
|
|
|
|
|
|
def test_sidebar_source_varies_cache_key():
|
|
key_webui = 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_cli = 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="cli",
|
|
)
|
|
key_omitted = 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=None,
|
|
)
|
|
|
|
assert key_webui != key_cli
|
|
assert key_webui != key_omitted
|
|
assert key_cli != key_omitted
|
|
|
|
|
|
def test_frontend_sends_sidebar_source_param():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _requestedSessionSidebarSource()" in src
|
|
assert "function _sessionListQueryString()" in src
|
|
assert "qs.set('sidebar_source', _requestedSessionSidebarSource());" in src
|
|
assert "_serverWebuiSessionCount" in src
|
|
assert "_serverCliSessionCount" in src
|
|
assert "function _sessionSourceTabCount(" in src
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_session_list_query_string_respects_sidebar_source_and_flags():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
|
|
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
|
|
archive_filter_fn = _extract_function(src, "_sessionArchivePagingFilterActive")
|
|
query_fn = _extract_function(src, "_sessionListQueryString")
|
|
script = f"""
|
|
global.window = {{ _showCliSessions: true }};
|
|
global._activeProject = null;
|
|
global._sessionSourceFilter = 'cli';
|
|
global._showAllProfiles = true;
|
|
global._showArchived = false;
|
|
global.SESSION_ARCHIVED_PAGE_SIZE = 100;
|
|
global.SESSION_ARCHIVED_MAX_LOADED_LIMIT = 2000;
|
|
global._archivedRowsLoadedLimit = 100;
|
|
global.NO_PROJECT_FILTER = '__none__';
|
|
let searchValue = '';
|
|
global.$ = (id) => id === 'sessionSearch' ? {{ value: searchValue }} : null;
|
|
{requested_source_fn}
|
|
{exclude_hidden_fn}
|
|
{archive_filter_fn}
|
|
{query_fn}
|
|
const first = _sessionListQueryString();
|
|
window._showCliSessions = false;
|
|
global._showArchived = true;
|
|
const second = _sessionListQueryString();
|
|
searchValue = 'old archived title';
|
|
const searchFiltered = _sessionListQueryString();
|
|
searchValue = '';
|
|
global._activeProject = 'project-1';
|
|
const projectFiltered = _sessionListQueryString();
|
|
global._activeProject = null;
|
|
global._archivedRowsLoadedLimit = 2500;
|
|
const capped = _sessionListQueryString();
|
|
global._activeProject = '__none__';
|
|
global._showAllProfiles = false;
|
|
global._showArchived = false;
|
|
const third = _sessionListQueryString();
|
|
console.log(JSON.stringify({{ first, second, searchFiltered, projectFiltered, capped, third }}));
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["first"] == "?sidebar_source=cli&exclude_hidden=1&all_profiles=1"
|
|
assert body["second"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1&archived_limit=100"
|
|
assert body["searchFiltered"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1"
|
|
assert body["projectFiltered"] == "?sidebar_source=webui&all_profiles=1&include_archived=1"
|
|
assert body["capped"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1&archived_limit=2000"
|
|
assert body["third"] == "?sidebar_source=webui&exclude_hidden=1"
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_archived_search_input_refetches_uncapped_then_restores_paging():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
|
|
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
|
|
archive_filter_fn = _extract_function(src, "_sessionArchivePagingFilterActive")
|
|
query_fn = _extract_function(src, "_sessionListQueryString")
|
|
sync_archive_fn = _extract_function(src, "_syncArchivedSearchPagingRefresh")
|
|
filter_fn = _extract_function(src, "filterSessions")
|
|
script = f"""
|
|
global.window = {{ _showCliSessions: false }};
|
|
global._activeProject = null;
|
|
global.NO_PROJECT_FILTER = '__none__';
|
|
global._sessionSourceFilter = 'webui';
|
|
global._showAllProfiles = false;
|
|
global._showArchived = true;
|
|
global.SESSION_ARCHIVED_PAGE_SIZE = 100;
|
|
global.SESSION_ARCHIVED_MAX_LOADED_LIMIT = 2000;
|
|
global._archivedRowsLoadedLimit = 100;
|
|
global._archivedSearchPagingQueryActive = false;
|
|
global._lastSessionSearchQuery = '';
|
|
global._hideSearchPreviewsAfterSelect = false;
|
|
global._contentSearchResults = [];
|
|
global._searchDebounceTimer = null;
|
|
const calls = [];
|
|
let searchValue = '';
|
|
global.$ = (id) => id === 'sessionSearch' ? {{ value: searchValue }} : null;
|
|
global.syncSessionSearchClear = () => {{}};
|
|
global.renderSessionList = () => {{ calls.push(_sessionListQueryString()); return Promise.resolve(); }};
|
|
global.renderSessionListFromCache = () => {{}};
|
|
global.clearTimeout = () => {{}};
|
|
global.setTimeout = () => 1;
|
|
global.api = () => Promise.resolve({{ sessions: [] }});
|
|
{requested_source_fn}
|
|
{exclude_hidden_fn}
|
|
{archive_filter_fn}
|
|
{query_fn}
|
|
{sync_archive_fn}
|
|
{filter_fn}
|
|
searchValue = 'page two title';
|
|
filterSessions();
|
|
searchValue = '';
|
|
filterSessions();
|
|
console.log(JSON.stringify({{ calls }}));
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["calls"] == [
|
|
"?sidebar_source=webui&exclude_hidden=1&include_archived=1",
|
|
"?sidebar_source=webui&exclude_hidden=1&include_archived=1&archived_limit=100",
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_session_source_switch_fetches_selected_bucket():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
fn_source = _extract_function(src, "_setSessionSourceFilter")
|
|
script = f"""
|
|
const renderCalls = [];
|
|
global._sessionSourceFilter = 'webui';
|
|
global._activeProject = 'demo-project';
|
|
global._selectedSessions = new Set(['first', 'second']);
|
|
global._sessionSelectMode = true;
|
|
global.localStorage = {{
|
|
writes: [],
|
|
setItem(key, value) {{
|
|
this.writes.push([key, value]);
|
|
}},
|
|
}};
|
|
global.renderSessionListFromCache = () => {{
|
|
renderCalls.push('cache');
|
|
}};
|
|
global.renderSessionList = (opts) => {{
|
|
renderCalls.push(opts);
|
|
return Promise.resolve();
|
|
}};
|
|
{fn_source}
|
|
_setSessionSourceFilter('cli');
|
|
console.log(JSON.stringify({{
|
|
sourceFilter: global._sessionSourceFilter,
|
|
activeProject: global._activeProject,
|
|
selectedSize: global._selectedSessions.size,
|
|
sessionSelectMode: global._sessionSelectMode,
|
|
storageWrites: global.localStorage.writes,
|
|
renderCalls,
|
|
}}));
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["sourceFilter"] == "cli"
|
|
assert body["activeProject"] is None
|
|
assert body["selectedSize"] == 0
|
|
assert body["sessionSelectMode"] is False
|
|
assert body["storageWrites"] == [["hermes-session-source-filter", "cli"]]
|
|
assert body["renderCalls"] == ["cache", {"deferWhileInteracting": False}]
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_apply_payload_and_tab_count_helpers_cover_old_and_new_payloads():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
|
|
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
|
|
apply_fn = _extract_function(src, "_applySessionListPayload")
|
|
count_fn = _extract_function(src, "_sessionSourceTabCount")
|
|
clear_fn = _extract_function(src, "_clearSessionSourceTabCounts")
|
|
script = f"""
|
|
global.window = {{ _showCliSessions: true }};
|
|
global._otherProfileCount = 0;
|
|
global._archivedWebuiCount = 0;
|
|
global._archivedCliCount = 0;
|
|
global._serverWebuiSessionCount = null;
|
|
global._serverCliSessionCount = null;
|
|
global._serverTimeDelta = 0;
|
|
global._serverTz = null;
|
|
global._optimisticallyRemovedSessionIds = new Set();
|
|
global._allSessions = [];
|
|
global._allSessionsScope = null;
|
|
global._allProjects = [];
|
|
global._sessionListLoadError = null;
|
|
global._sessionListHasLoadedOnce = false;
|
|
global._sessionListFirstRenderAnimated = true;
|
|
global._sessionListSkeletonActive = true;
|
|
global._sessionListRefreshAnimationPending = false;
|
|
global._lastSessionListRenderSig = null;
|
|
global._activeProject = null;
|
|
global.NO_PROJECT_FILTER = '__none__';
|
|
global._showAllProfiles = false;
|
|
global._sessionSourceFilter = 'webui';
|
|
global._renamingSid = null;
|
|
global._sessionActionMenu = null;
|
|
global.S = {{ activeProfile: 'default' }};
|
|
global._reconcileActiveSessionIdleStateFromList = rows => rows;
|
|
global._mergeOptimisticFirstTurnSessions = rows => rows;
|
|
global._sessionListRenderSignature = () => '';
|
|
global._purgeStaleInflightEntries = () => {{}};
|
|
global._syncSessionAttentionSoundState = () => {{}};
|
|
global._pruneLineageReportCacheToVisibleSessions = () => {{}};
|
|
global._markPollingCompletionUnreadTransitions = () => {{}};
|
|
global._recordSessionProfileCount = () => {{}};
|
|
global._isSessionEffectivelyStreaming = () => false;
|
|
global.startStreamingPoll = () => {{}};
|
|
global.stopStreamingPoll = () => {{}};
|
|
global.ensureSessionTimeRefreshPoll = () => {{}};
|
|
global.ensureActiveSessionExternalRefreshPoll = () => {{}};
|
|
global.ensureSessionEventsSSE = () => {{}};
|
|
global.animateNextSessionListRefresh = () => {{}};
|
|
global.renderSessionListFromCache = () => {{}};
|
|
{clear_fn}
|
|
{count_fn}
|
|
{requested_source_fn}
|
|
{exclude_hidden_fn}
|
|
{apply_fn}
|
|
const sessions = [{{ session_id: 'webui-1' }}];
|
|
_applySessionListPayload({{ sessions, other_profile_count: 0, archived_count: 0, active_profile: 'default' }}, {{ projects: [] }});
|
|
const oldPayload = {{
|
|
webui: _sessionSourceTabCount('webui', 7, 3),
|
|
cli: _sessionSourceTabCount('cli', 7, 3),
|
|
}};
|
|
_clearSessionSourceTabCounts();
|
|
_applySessionListPayload({{
|
|
sessions,
|
|
other_profile_count: 0,
|
|
archived_count: 0,
|
|
active_profile: 'default',
|
|
webui_session_count: 11,
|
|
cli_session_count: 5,
|
|
}}, {{ projects: [] }});
|
|
const newPayload = {{
|
|
webui: _sessionSourceTabCount('webui', 7, 3),
|
|
cli: _sessionSourceTabCount('cli', 7, 3),
|
|
}};
|
|
console.log(JSON.stringify({{ oldPayload, newPayload }}));
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["oldPayload"] == {"webui": 7, "cli": 3}
|
|
assert body["newPayload"] == {"webui": 11, "cli": 5}
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_source_filtered_cache_preserves_hidden_bucket_runtime_state():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
is_cli_fn = _extract_function(src, "_isCliSession")
|
|
remember_source_fn = _extract_function(src, "_rememberSessionListSource")
|
|
remember_streaming_fn = _extract_function(src, "_rememberRenderedStreamingState")
|
|
remember_snapshot_fn = _extract_function(src, "_rememberRenderedSessionSnapshot")
|
|
purge_fn = _extract_function(src, "_hasOwnedOpenLiveStream") + "\n" + _extract_function(src, "_purgeStaleInflightEntries")
|
|
mark_fn = _extract_function(src, "_markPollingCompletionUnreadTransitions")
|
|
script = f"""
|
|
global._allSessions = [{{
|
|
session_id: 'cli-1',
|
|
source_tag: 'cli',
|
|
raw_source: 'cli',
|
|
session_source: 'cli',
|
|
is_streaming: false,
|
|
message_count: 1,
|
|
last_message_at: 10,
|
|
}}];
|
|
global._allSessionsScope = {{ sidebarSource: 'cli' }};
|
|
global._sessionListSourceById = new Map([['webui-live', 'webui']]);
|
|
global._sessionStreamingById = new Map([['webui-live', true]]);
|
|
global._sessionListSnapshotById = new Map([['webui-live', {{ message_count: 1, last_message_at: 1 }}]]);
|
|
global._sendInProgress = false;
|
|
global._sendInProgressSid = null;
|
|
global.INFLIGHT = {{ 'webui-live': {{ lastAssistantText: 'working' }} }};
|
|
const cleared = [];
|
|
global.clearInflightState = sid => cleared.push(sid);
|
|
global._isSessionEffectivelyStreaming = s => Boolean(s.is_streaming);
|
|
global._getSessionObservedStreaming = () => ({{}});
|
|
global._hasPendingUserMessageSignal = () => false;
|
|
global._isSessionActivelyViewedForList = () => false;
|
|
global._markSessionCompletionUnread = () => {{}};
|
|
global._setSessionViewedCount = () => {{}};
|
|
global._rememberObservedStreamingSession = () => {{}};
|
|
global._forgetObservedStreamingSession = () => {{}};
|
|
{is_cli_fn}
|
|
{remember_source_fn}
|
|
{remember_streaming_fn}
|
|
{remember_snapshot_fn}
|
|
{purge_fn}
|
|
{mark_fn}
|
|
const cliStale = {{
|
|
session_id: 'cli-stale',
|
|
source_tag: 'cli',
|
|
raw_source: 'cli',
|
|
session_source: 'cli',
|
|
is_streaming: false,
|
|
message_count: 2,
|
|
last_message_at: 2,
|
|
}};
|
|
_rememberRenderedStreamingState(cliStale, true);
|
|
_rememberRenderedSessionSnapshot(cliStale);
|
|
INFLIGHT['cli-stale'] = {{ lastAssistantText: 'stale' }};
|
|
_purgeStaleInflightEntries();
|
|
_markPollingCompletionUnreadTransitions(global._allSessions);
|
|
console.log(JSON.stringify({{
|
|
inflightKeys: Object.keys(INFLIGHT),
|
|
cleared,
|
|
streamingKeys: Array.from(_sessionStreamingById.keys()).sort(),
|
|
snapshotKeys: Array.from(_sessionListSnapshotById.keys()).sort(),
|
|
sourceKeys: Array.from(_sessionListSourceById.keys()).sort(),
|
|
}}));
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["inflightKeys"] == ["webui-live"]
|
|
assert body["cleared"] == ["cli-stale"]
|
|
assert "webui-live" in body["streamingKeys"]
|
|
assert "cli-stale" not in body["streamingKeys"]
|
|
assert "webui-live" in body["snapshotKeys"]
|
|
assert "cli-stale" not in body["snapshotKeys"]
|
|
assert "webui-live" in body["sourceKeys"]
|
|
assert "cli-stale" not in body["sourceKeys"]
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_sid_only_source_remembering_skips_scope_fallback():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
is_cli_fn = _extract_function(src, "_isCliSession")
|
|
remember_source_fn = _extract_function(src, "_rememberSessionListSource")
|
|
script = f"""
|
|
global._allSessions = [];
|
|
global._allSessionsScope = {{ sidebarSource: 'cli' }};
|
|
global._sessionListSourceById = new Map();
|
|
{is_cli_fn}
|
|
{remember_source_fn}
|
|
_rememberSessionListSource(null, 'detached-sid', false);
|
|
console.log(JSON.stringify({{
|
|
hasDetached: _sessionListSourceById.has('detached-sid'),
|
|
remembered: Array.from(_sessionListSourceById.entries()),
|
|
}}));
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["hasDetached"] is False
|
|
assert body["remembered"] == []
|
|
|
|
|
|
def test_session_list_response_omits_bucket_counts_when_missing(monkeypatch):
|
|
monkeypatch.setattr(routes, "_session_list_cache_overlay_runtime_rows", lambda rows: rows)
|
|
monkeypatch.setattr(routes, "_sidebar_session_response_item", lambda row, *, redact_enabled=None: row)
|
|
|
|
body = routes._session_list_payload_to_response(
|
|
{
|
|
"sessions": [{"session_id": "webui-1", "title": "WebUI Session"}],
|
|
"cli_count": 0,
|
|
"archived_count": 0,
|
|
"archived_webui_count": 0,
|
|
"archived_cli_count": 0,
|
|
"include_archived": False,
|
|
"all_profiles": False,
|
|
"active_profile": "default",
|
|
"other_profile_count": 0,
|
|
}
|
|
)
|
|
|
|
assert "webui_session_count" not in body
|
|
assert "cli_session_count" not in body
|
|
assert body["sessions"][0]["session_id"] == "webui-1"
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_scope_mismatch_error_path_respects_sidebar_source():
|
|
src = SESSIONS_JS.read_text(encoding="utf-8")
|
|
purge_fn = _extract_function(src, "_hasOwnedOpenLiveStream") + "\n" + _extract_function(src, "_purgeStaleInflightEntries")
|
|
clear_fn = _extract_function(src, "_clearSessionSourceTabCounts")
|
|
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
|
|
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
|
|
query_fn = _extract_function(src, "_sessionListQueryString")
|
|
fetch_helper_fn = _ensure_async(
|
|
_extract_function(src, "_loadSidebarSessionListPayload"),
|
|
"_loadSidebarSessionListPayload",
|
|
)
|
|
refresh_fn = _ensure_async(
|
|
_extract_function(src, "_runRenderSessionListRefresh"),
|
|
"_runRenderSessionListRefresh",
|
|
)
|
|
script = f"""
|
|
global.window = {{ _showCliSessions: true }};
|
|
global._showAllProfiles = false;
|
|
global._showArchived = false;
|
|
global._sessionListHasLoadedOnce = true;
|
|
global._SESSION_LIST_BOOT_TIMEOUT_MS = 90000;
|
|
global._renderSessionListGen = 1;
|
|
global._profileSwitchListEmbargo = false;
|
|
global._pendingSessionListPayload = null;
|
|
global._allProjects = [];
|
|
global._contentSearchResults = ['stale'];
|
|
global._activeProject = null;
|
|
global.NO_PROJECT_FILTER = '__none__';
|
|
global.S = {{ activeProfile: 'default' }};
|
|
global.$ = () => ({{ value: '' }});
|
|
global._isSessionListUserInteracting = () => false;
|
|
global._schedulePendingSessionListApply = () => {{}};
|
|
global._showSessionListLoadError = error => {{
|
|
global._lastError = error.message;
|
|
}};
|
|
const renders = [];
|
|
const cleared = [];
|
|
global.renderSessionListFromCache = () => {{
|
|
_purgeStaleInflightEntries();
|
|
renders.push({{
|
|
sessions: Array.isArray(global._allSessions) ? global._allSessions.map(s => s.session_id) : null,
|
|
scope: global._allSessionsScope ? {{ ...global._allSessionsScope }} : null,
|
|
webui: global._serverWebuiSessionCount,
|
|
cli: global._serverCliSessionCount,
|
|
skeleton: global._sessionListSkeletonActive,
|
|
inflightKeys: Object.keys(global.INFLIGHT || {{}}).sort(),
|
|
}});
|
|
}};
|
|
global.api = () => Promise.reject(new Error('boom'));
|
|
global.clearInflightState = sid => cleared.push(sid);
|
|
{purge_fn}
|
|
{clear_fn}
|
|
{requested_source_fn}
|
|
{exclude_hidden_fn}
|
|
{query_fn}
|
|
{fetch_helper_fn}
|
|
{refresh_fn}
|
|
async function runCase(requestedSource, cachedSource) {{
|
|
global._sessionSourceFilter = requestedSource;
|
|
global._allSessions = [{{ session_id: cachedSource + '-1' }}];
|
|
global._allSessionsScope = {{
|
|
profile: 'default',
|
|
allProfiles: false,
|
|
sidebarSource: cachedSource,
|
|
excludeHidden: true,
|
|
}};
|
|
global._sessionListSourceById = new Map([['webui-live', 'webui']]);
|
|
global.INFLIGHT = {{ 'webui-live': {{ lastAssistantText: 'working' }} }};
|
|
cleared.length = 0;
|
|
global._serverWebuiSessionCount = 11;
|
|
global._serverCliSessionCount = 5;
|
|
global._sessionListSkeletonActive = true;
|
|
global._lastError = null;
|
|
renders.length = 0;
|
|
await _runRenderSessionListRefresh({{}}, 1);
|
|
return {{
|
|
sessions: Array.isArray(global._allSessions) ? global._allSessions.map(s => s.session_id) : null,
|
|
scope: global._allSessionsScope ? {{ ...global._allSessionsScope }} : null,
|
|
webui: global._serverWebuiSessionCount,
|
|
cli: global._serverCliSessionCount,
|
|
skeleton: global._sessionListSkeletonActive,
|
|
error: global._lastError,
|
|
cleared: [...cleared],
|
|
inflightKeys: Object.keys(global.INFLIGHT || {{}}).sort(),
|
|
render: renders[0] || null,
|
|
}};
|
|
}}
|
|
(async () => {{
|
|
const mismatch = await runCase('cli', 'webui');
|
|
const match = await runCase('webui', 'webui');
|
|
console.log(JSON.stringify({{ mismatch, match }}));
|
|
}})().catch(error => {{
|
|
console.error(error);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
body = _run_node(script)
|
|
|
|
assert body["mismatch"]["sessions"] == []
|
|
assert body["mismatch"]["scope"] == {
|
|
"profile": "default",
|
|
"allProfiles": False,
|
|
"sidebarSource": "cli",
|
|
"excludeHidden": True,
|
|
}
|
|
assert body["mismatch"]["webui"] is None
|
|
assert body["mismatch"]["cli"] is None
|
|
assert body["mismatch"]["skeleton"] is False
|
|
assert body["mismatch"]["render"]["sessions"] == []
|
|
assert body["mismatch"]["inflightKeys"] == ["webui-live"]
|
|
assert body["mismatch"]["cleared"] == []
|
|
assert body["mismatch"]["render"]["inflightKeys"] == ["webui-live"]
|
|
assert body["match"]["sessions"] == ["webui-1"]
|
|
assert body["match"]["scope"] == {
|
|
"profile": "default",
|
|
"allProfiles": False,
|
|
"sidebarSource": "webui",
|
|
"excludeHidden": True,
|
|
}
|
|
assert body["match"]["webui"] == 11
|
|
assert body["match"]["cli"] == 5
|
|
assert body["match"]["render"]["sessions"] == ["webui-1"]
|
|
|
|
|
|
def test_payload_row_count_regression(monkeypatch):
|
|
rows = _session_rows(webui_count=30, cli_count=20)
|
|
_install_common_monkeypatches(monkeypatch, rows)
|
|
|
|
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui")
|
|
body = handler.json_body()
|
|
|
|
assert handler.status == 200
|
|
assert len(body["sessions"]) == 30
|