"""Regression tests for #4759: parallelize first-load sidebar boot fetches.""" from __future__ import annotations import json import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8") NODE = shutil.which("node") def _extract_function(source_text: str, function_name: str) -> str: marker = f"async function {function_name}(" start = source_text.find(marker) if start < 0: marker = f"function {function_name}(" start = source_text.find(marker) assert start >= 0, f"{function_name}() not found" brace_start = source_text.find("{", start) assert brace_start >= 0, f"{function_name} body not found" depth = 0 in_string = None escaped = False in_line_comment = False in_block_comment = False for index in range(brace_start, len(source_text)): char = source_text[index] nxt = source_text[index + 1] if index + 1 < len(source_text) else "" if in_line_comment: if char == "\n": in_line_comment = False continue if in_block_comment: if char == "*" and nxt == "/": in_block_comment = False continue if in_string: if escaped: escaped = False elif char == "\\": escaped = True elif char == in_string: in_string = None continue if char == "/" and nxt == "/": in_line_comment = True continue if char == "/" and nxt == "*": in_block_comment = True continue if char in ("'", '"', "`"): in_string = char continue 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: str): completed = subprocess.run( [NODE, "-e", script], cwd=ROOT, text=True, capture_output=True, check=False, ) assert completed.returncode == 0, completed.stderr or completed.stdout return json.loads(completed.stdout.strip()) @pytest.mark.skipif(NODE is None, reason="node not on PATH") def test_cold_boot_starts_projects_fetch_before_sessions_resolve(): """The project request must start before the session request settles.""" fetch_helper = _extract_function(SESSIONS_JS, "_loadSidebarSessionListPayload") script = f""" const calls = []; let resolveSessions; let resolveProjects; global._showAllProfiles = false; global._allProjects = []; global._sessionListHasLoadedOnce = false; global.api = (url) => {{ if (url.startsWith('/api/projects')) {{ calls.push('projects'); return new Promise(resolve => {{ resolveProjects = resolve; }}); }} if (url.startsWith('/api/sessions')) {{ calls.push('sessions'); return new Promise(resolve => {{ resolveSessions = resolve; }}); }} return Promise.reject(new Error('unexpected endpoint ' + url)); }}; {fetch_helper} (async () => {{ const run = _loadSidebarSessionListPayload('?v=1', {{ timeoutToast:false, timeoutMs:90000, retries:1, retryTimeouts:true, retryStatuses:[502,503,504], }}); await Promise.resolve(); const orderAtSettleBoundary = calls.slice(); resolveSessions({{ sessions:[{{session_id:'s1'}}], other_profile_count:0, archived_count:0, archived_webui_count:0, archived_cli_count:0, }}); resolveProjects({{projects:[]}}); const payload = await run; console.log(JSON.stringify({{orderAtSettleBoundary,payload,calls:calls}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ body = _run_node(script) assert body["orderAtSettleBoundary"] == ["projects", "sessions"] assert body["payload"]["sessData"]["sessions"] == [{"session_id": "s1"}] assert body["payload"]["projData"]["projects"] == [] @pytest.mark.skipif(NODE is None, reason="node not on PATH") def test_project_failure_falls_back_without_blocking_session_payload(): """Project endpoint failure should still return session payload and fallback projects.""" fetch_helper = _extract_function(SESSIONS_JS, "_loadSidebarSessionListPayload") script = f""" const calls = []; global._showAllProfiles = true; global._allProjects = []; global._sessionListHasLoadedOnce = false; global.api = (url) => {{ if (url.startsWith('/api/projects')) {{ calls.push('projects'); return Promise.reject(new Error('project endpoint down')); }} if (url.startsWith('/api/sessions')) {{ calls.push('sessions'); return Promise.resolve({{ sessions:[{{session_id:'s1'}}], other_profile_count:0, archived_count:0, archived_webui_count:0, archived_cli_count:0, }}); }} return Promise.reject(new Error('unexpected endpoint ' + url)); }}; {fetch_helper} (async () => {{ const payload = await _loadSidebarSessionListPayload('?v=1', {{ timeoutToast:false, timeoutMs:90000, retries:1, retryTimeouts:true, retryStatuses:[502,503,504], }}); console.log(JSON.stringify({{calls, payload}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ body = _run_node(script) assert body["calls"] == ["projects", "sessions"] assert body["payload"]["sessData"]["sessions"][0]["session_id"] == "s1" assert body["payload"]["projData"]["projects"] == [] @pytest.mark.skipif(NODE is None, reason="node not on PATH") def test_existing_session_request_options_are_preserved(): """The cold-load /api/sessions request keeps the existing timeout/retry policy.""" fetch_helper = _extract_function(SESSIONS_JS, "_loadSidebarSessionListPayload") refresh_fn = _extract_function(SESSIONS_JS, "_runRenderSessionListRefresh") script = f""" global._SESSION_LIST_BOOT_TIMEOUT_MS = 90000; global._sessionListHasLoadedOnce = false; global._renderSessionListGen = 1; global._profileSwitchListEmbargo = false; global._pendingSessionListPayload = null; global._showAllProfiles = false; global._allProjects = []; global._contentSearchResults = []; global.$ = () => ({{ value: '' }}); global._isSessionListUserInteracting = () => false; global._schedulePendingSessionListApply = () => {{}}; global._showSessionListLoadError = error => {{ throw new Error(error && error.message ? error.message : String(error)); }}; global._applySessionListPayload = () => {{}}; global._sessionListQueryString = () => '?v=1'; const calls = []; global.api = (url, opts) => {{ calls.push({{endpoint:url.replace('/api/',''), opts}}); if(url.startsWith('/api/sessions')){{ return Promise.resolve({{ sessions:[{{session_id:'s1'}}], other_profile_count:0, archived_count:0, archived_webui_count:0, archived_cli_count:0, }}); }} return Promise.resolve({{projects:[]}}); }}; {fetch_helper} {refresh_fn} (async () => {{ await _runRenderSessionListRefresh({{}}, 1); const sessionCall = calls.find(entry => entry.endpoint === 'sessions?') || calls.find(entry => entry.endpoint.startsWith('sessions')); const projectCall = calls.find(entry => entry.endpoint === 'projects?') || calls.find(entry => entry.endpoint.startsWith('projects')); console.log(JSON.stringify({{ sessionOpts: sessionCall ? sessionCall.opts : null, projectOpts: projectCall ? projectCall.opts : null, calls, }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ body = _run_node(script) session_opts = body["sessionOpts"] or {} project_opts = body["projectOpts"] or {} assert session_opts["timeoutToast"] is False assert session_opts["timeoutMs"] == 90000 assert session_opts["retries"] == 1 assert session_opts["retryTimeouts"] is True assert session_opts["retryStatuses"] == [502, 503, 504] assert project_opts == {"timeoutToast": False} @pytest.mark.skipif(NODE is None, reason="node not on PATH") def test_warm_refresh_parallelizes_projects_fetch_and_keeps_minimal_session_opts(): """Warm refresh uses minimal session opts, but still starts projects immediately.""" fetch_helper = _extract_function(SESSIONS_JS, "_loadSidebarSessionListPayload") script = f""" const calls = []; let resolveSessions; let resolveProjects; global._showAllProfiles = false; global._allProjects = []; global._sessionListHasLoadedOnce = true; global.api = (url, opts) => {{ if (url.startsWith('/api/projects')) {{ calls.push({{ endpoint:'projects', opts: opts || null }}); return new Promise(resolve => {{ resolveProjects = resolve; }}); }} if (url.startsWith('/api/sessions')) {{ calls.push({{ endpoint:'sessions', opts: opts || null }}); return new Promise(resolve => {{ resolveSessions = resolve; }}); }} return Promise.reject(new Error('unexpected endpoint ' + url)); }}; {fetch_helper} (async () => {{ const run = _loadSidebarSessionListPayload('?v=1', {{ timeoutToast:false, }}); await Promise.resolve(); const orderAtSettleBoundary = calls.map(call => call.endpoint); const sessionOpts = calls.find(call => call.endpoint === 'sessions').opts; const projectOpts = calls.find(call => call.endpoint === 'projects').opts; resolveSessions({{ sessions:[{{session_id:'warm-1'}}], other_profile_count:0, archived_count:0, archived_webui_count:0, archived_cli_count:0, }}); resolveProjects({{projects:[{{name:'demo'}}]}}); const payload = await run; console.log(JSON.stringify({{ orderAtSettleBoundary, sessionOpts, projectOpts, payload, }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ body = _run_node(script) assert body["orderAtSettleBoundary"] == ["projects", "sessions"] assert body["sessionOpts"] == {"timeoutToast": False} assert body["projectOpts"] == {"timeoutToast": False} assert body["payload"]["sessData"]["sessions"] == [{"session_id": "warm-1"}] assert body["payload"]["projData"]["projects"] == [{"name": "demo"}]