fix(#4766): filter /api/sessions by active source bucket
This commit is contained in:
parent
fa3d612a4e
commit
9a5a3eb6d5
3 changed files with 285 additions and 7 deletions
|
|
@ -1598,6 +1598,7 @@ def _session_list_cache_key(
|
|||
show_cron_sessions: bool,
|
||||
include_archived: bool = False,
|
||||
source_filter: str | None = None,
|
||||
sidebar_source: str | None = None,
|
||||
) -> tuple:
|
||||
return (
|
||||
_session_list_cache_profile_scope(active_profile),
|
||||
|
|
@ -1607,6 +1608,7 @@ def _session_list_cache_key(
|
|||
bool(show_cron_sessions),
|
||||
bool(include_archived),
|
||||
source_filter,
|
||||
sidebar_source,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1925,6 +1927,7 @@ def _build_session_list_cache_payload(
|
|||
show_cron_sessions: bool,
|
||||
include_archived: bool = False,
|
||||
source_filter: str | None = None,
|
||||
sidebar_source: str | None = None,
|
||||
diag=None,
|
||||
) -> dict:
|
||||
diag_stage = diag.stage if diag is not None else lambda *_a, **_k: None
|
||||
|
|
@ -2097,6 +2100,18 @@ def _build_session_list_cache_payload(
|
|||
)
|
||||
archived_count = archived_webui_count + archived_cli_count
|
||||
scoped = archived_scoped if include_archived else visible_scoped
|
||||
webui_session_count = sum(
|
||||
1 for s in scoped
|
||||
if not _is_cli_session_for_settings(s)
|
||||
)
|
||||
cli_session_count = sum(
|
||||
1 for s in scoped
|
||||
if _is_cli_session_for_settings(s)
|
||||
)
|
||||
if sidebar_source == "webui":
|
||||
scoped = [s for s in scoped if not _is_cli_session_for_settings(s)]
|
||||
elif sidebar_source == "cli":
|
||||
scoped = [s for s in scoped if _is_cli_session_for_settings(s)]
|
||||
if not include_archived:
|
||||
diag_stage("filter_archived_sessions")
|
||||
diag_stage("visible_lineage_metadata")
|
||||
|
|
@ -2110,6 +2125,8 @@ def _build_session_list_cache_payload(
|
|||
"archived_count": archived_count,
|
||||
"archived_webui_count": archived_webui_count,
|
||||
"archived_cli_count": archived_cli_count,
|
||||
"webui_session_count": webui_session_count,
|
||||
"cli_session_count": cli_session_count,
|
||||
"include_archived": include_archived,
|
||||
"all_profiles": all_profiles,
|
||||
"active_profile": active_profile,
|
||||
|
|
@ -2134,6 +2151,8 @@ def _session_list_payload_to_response(payload: dict) -> dict:
|
|||
"archived_count": int(payload.get("archived_count", 0)),
|
||||
"archived_webui_count": int(payload.get("archived_webui_count", 0)),
|
||||
"archived_cli_count": int(payload.get("archived_cli_count", 0)),
|
||||
"webui_session_count": int(payload.get("webui_session_count", 0)),
|
||||
"cli_session_count": int(payload.get("cli_session_count", 0)),
|
||||
"include_archived": bool(payload.get("include_archived", False)),
|
||||
"all_profiles": bool(payload.get("all_profiles", False)),
|
||||
"active_profile": payload.get("active_profile"),
|
||||
|
|
@ -9733,6 +9752,9 @@ def handle_get(handler, parsed) -> bool:
|
|||
active_profile = get_active_profile_name()
|
||||
all_profiles = _all_profiles_enabled(parsed)
|
||||
include_archived = _query_flag(parsed, "include_archived")
|
||||
sidebar_source = parse_qs(parsed.query).get("sidebar_source", [""])[0].strip().lower() or None
|
||||
if sidebar_source not in ("webui", "cli"):
|
||||
sidebar_source = None
|
||||
key = _session_list_cache_key(
|
||||
active_profile=active_profile,
|
||||
all_profiles=all_profiles,
|
||||
|
|
@ -9741,6 +9763,7 @@ def handle_get(handler, parsed) -> bool:
|
|||
show_cron_sessions=show_cron_sessions,
|
||||
include_archived=include_archived,
|
||||
source_filter=agent_session_source_filter,
|
||||
sidebar_source=sidebar_source,
|
||||
)
|
||||
# Keep the visible /api/sessions contract unchanged even though the
|
||||
# heavy lifting now lives in the cache builder: profile scoping via
|
||||
|
|
@ -9756,6 +9779,7 @@ def handle_get(handler, parsed) -> bool:
|
|||
show_cron_sessions=show_cron_sessions,
|
||||
include_archived=include_archived,
|
||||
source_filter=agent_session_source_filter,
|
||||
sidebar_source=sidebar_source,
|
||||
diag=diag,
|
||||
),
|
||||
diag=diag,
|
||||
|
|
|
|||
|
|
@ -2673,6 +2673,8 @@ let _showAllProfiles = false; // false = filter to active profile only
|
|||
let _otherProfileCount = 0; // count of sessions from other profiles (server-reported)
|
||||
let _archivedWebuiCount = 0; // archived WebUI sessions not fetched until requested
|
||||
let _archivedCliCount = 0; // archived non-WebUI sessions not fetched until requested
|
||||
let _serverWebuiSessionCount = null; // explicit server count for WebUI sessions
|
||||
let _serverCliSessionCount = null; // explicit server count for CLI sessions
|
||||
let _sessionSourceFilter = 'webui'; // 'webui' keeps WebUI chats separate from read-only CLI sessions
|
||||
_restoreSessionSourceFilter();
|
||||
let _sessionActionMenu = null;
|
||||
|
|
@ -3746,6 +3748,14 @@ function _applySessionListPayload(sessData, projData){
|
|||
_otherProfileCount = sessData.other_profile_count || 0;
|
||||
_archivedWebuiCount = Number(sessData.archived_webui_count ?? sessData.archived_count ?? 0);
|
||||
_archivedCliCount = Number(sessData.archived_cli_count ?? 0);
|
||||
_serverWebuiSessionCount = Object.prototype.hasOwnProperty.call(sessData, 'webui_session_count')
|
||||
? Number(sessData.webui_session_count)
|
||||
: null;
|
||||
_serverCliSessionCount = Object.prototype.hasOwnProperty.call(sessData, 'cli_session_count')
|
||||
? Number(sessData.cli_session_count)
|
||||
: null;
|
||||
if (!Number.isFinite(_serverWebuiSessionCount)) _serverWebuiSessionCount = null;
|
||||
if (!Number.isFinite(_serverCliSessionCount)) _serverCliSessionCount = null;
|
||||
// Capture server clock for clock-skew compensation (issue #1144).
|
||||
// server_time is epoch seconds from the server's time.time().
|
||||
// _serverTimeDelta = client - server, so (Date.now() - _serverTimeDelta)
|
||||
|
|
@ -3827,6 +3837,8 @@ async function _runRenderSessionListRefresh(opts, _gen){
|
|||
try{
|
||||
if(!($('sessionSearch').value||'').trim()) _contentSearchResults = [];
|
||||
const qs = new URLSearchParams();
|
||||
const requestSidebarSource = window._showCliSessions ? _sessionSourceFilter : 'webui';
|
||||
qs.set('sidebar_source', requestSidebarSource);
|
||||
if(_showAllProfiles) qs.set('all_profiles','1');
|
||||
if(_showArchived) qs.set('include_archived','1');
|
||||
const sessionListQS = qs.toString() ? `?${qs.toString()}` : '';
|
||||
|
|
@ -5550,12 +5562,14 @@ function renderSessionListFromCache(){
|
|||
}=_partitionSidebarSessionRows(allMatched, activeSidForSidebar);
|
||||
const referenceRaw=_sessionSourceFilter==='cli'?cliReferenceRaw:webuiReferenceRaw;
|
||||
const sessions=_renderSidebarRowsFromRawSessions(sessionsRaw, referenceRaw);
|
||||
const renderedWebuiSessionCount=_sessionSourceFilter==='webui'
|
||||
? sessions.length
|
||||
: _renderSidebarRowsFromRawSessions(webuiSessionsRaw, webuiReferenceRaw).length;
|
||||
const renderedCliSessionCount=_sessionSourceFilter==='cli'
|
||||
? sessions.length
|
||||
: _renderSidebarRowsFromRawSessions(cliSessionsRaw, cliReferenceRaw).length;
|
||||
const renderedWebuiSessionCount=_renderSidebarRowsFromRawSessions(webuiSessionsRaw, webuiReferenceRaw).length;
|
||||
const renderedCliSessionCount=_renderSidebarRowsFromRawSessions(cliSessionsRaw, cliReferenceRaw).length;
|
||||
const webuiSessionTabCount=Number.isFinite(_serverWebuiSessionCount)
|
||||
? _serverWebuiSessionCount
|
||||
: renderedWebuiSessionCount;
|
||||
const cliSessionTabCount=Number.isFinite(_serverCliSessionCount)
|
||||
? _serverCliSessionCount
|
||||
: renderedCliSessionCount;
|
||||
_syncSidebarExpansionForActiveSession(sessions, activeSidForSidebar);
|
||||
const list=$('sessionList');
|
||||
const animateRefresh=_sessionListRefreshAnimationPending;
|
||||
|
|
@ -5616,7 +5630,7 @@ function renderSessionListFromCache(){
|
|||
const sourceTabs=document.createElement('div');
|
||||
sourceTabs.className='session-source-tabs';
|
||||
for(const filter of ['webui','cli']){
|
||||
const count=filter==='cli'?renderedCliSessionCount:renderedWebuiSessionCount;
|
||||
const count=filter==='cli'?cliSessionTabCount:webuiSessionTabCount;
|
||||
const btn=document.createElement('button');
|
||||
btn.type='button';
|
||||
btn.className='session-source-tab'+(_sessionSourceFilter===filter?' active':'');
|
||||
|
|
|
|||
240
tests/test_issue4766_sidebar_source_pushdown.py
Normal file
240
tests/test_issue4766_sidebar_source_pushdown.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
"""Regression coverage for issue #4766: `/api/sessions` filters by active sidebar source."""
|
||||
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
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"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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_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 "const requestSidebarSource = window._showCliSessions ? _sessionSourceFilter : 'webui';" in src
|
||||
assert "qs.set('sidebar_source', requestSidebarSource);" in src
|
||||
assert "_serverWebuiSessionCount" in src
|
||||
assert "_serverCliSessionCount" in src
|
||||
assert "Number.isFinite(_serverWebuiSessionCount)" in src
|
||||
assert "Number.isFinite(_serverCliSessionCount)" in src
|
||||
assert "_sessionSourceLabel(filter,count)" in src
|
||||
|
||||
|
||||
def test_frontend_avoids_cli_bucket_request_when_cli_hidden():
|
||||
src = SESSIONS_JS.read_text(encoding="utf-8")
|
||||
|
||||
assert "window._showCliSessions ? _sessionSourceFilter : 'webui'" in src
|
||||
|
||||
|
||||
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
|
||||
Loading…
Reference in a new issue