Files
hermes-webui/tests/test_issue4759_parallel_sidebar_boot_fetch.py
Frank SongandPaperclip 47d8ac94d3 Harden connection flapping aftercare
Keep sidebar retries boot-scoped, preserve immediate source-change cache rebuilds, and tighten session stream ownership before closing duplicate SSE sockets.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 19:52:02 +00:00

327 lines
10 KiB
Python

"""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"}]