78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
|
|
"""Regression tests for #3993 — self-heal stuck session loads on non-404 failures.
|
||
|
|
|
||
|
|
A session load that fails with a non-404, non-401 error (400 / 403 / 500 /
|
||
|
|
network) during boot used to leave a stale session id in localStorage + URL, so
|
||
|
|
every subsequent refresh retried the same dead session and the WebUI could not
|
||
|
|
load ANY chat. loadSession() now calls _clearStuckSessionOnBoot(sid, currentSid)
|
||
|
|
which clears the stale id ONLY when no session is currently on screen
|
||
|
|
(!currentSid) — never when the user is already viewing a healthy session (a
|
||
|
|
500/network blip there may be transient, #4028), and never on a click into a
|
||
|
|
*different* dead session (localStorage/URL still point at the live one, #2782).
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
REPO = Path(__file__).resolve().parent.parent
|
||
|
|
SESSIONS_JS = REPO / "static" / "sessions.js"
|
||
|
|
NODE = shutil.which("node")
|
||
|
|
|
||
|
|
|
||
|
|
def _read(p: Path) -> str:
|
||
|
|
return p.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def test_clear_stuck_session_helper_exists_and_is_wired():
|
||
|
|
"""The non-404 failure branch must delegate to _clearStuckSessionOnBoot."""
|
||
|
|
js = _read(SESSIONS_JS)
|
||
|
|
marker = "function _clearStuckSessionOnBoot(sid, currentSid){"
|
||
|
|
assert marker in js
|
||
|
|
# Wired into the non-404 error branch.
|
||
|
|
assert "_clearStuckSessionOnBoot(sid, currentSid);" in js
|
||
|
|
# Guarded on the boot condition (no active session on screen).
|
||
|
|
helper = js[js.index(marker): js.index(marker) + 260]
|
||
|
|
assert "if(!currentSid){" in helper
|
||
|
|
assert "localStorage.removeItem('hermes-webui-session')" in helper
|
||
|
|
assert "history.replaceState" in helper
|
||
|
|
|
||
|
|
|
||
|
|
def _run_helper(current_sid_js: str) -> dict:
|
||
|
|
"""Run _clearStuckSessionOnBoot in node with a fake localStorage/history and
|
||
|
|
report whether each was cleared."""
|
||
|
|
assert NODE, "node is required"
|
||
|
|
js = _read(SESSIONS_JS)
|
||
|
|
start = js.index("function _clearStuckSessionOnBoot(sid, currentSid){")
|
||
|
|
end = js.index("\n}", start) + 2
|
||
|
|
helper_src = js[start:end]
|
||
|
|
script = f"""
|
||
|
|
let removed=false, replaced=false;
|
||
|
|
const localStorage = {{ removeItem(k){{ if(k==='hermes-webui-session') removed=true; }} }};
|
||
|
|
const history = {{ replaceState(){{ replaced=true; }} }};
|
||
|
|
function _appRootPath(){{ return '/'; }}
|
||
|
|
{helper_src}
|
||
|
|
_clearStuckSessionOnBoot('dead-sid', {current_sid_js});
|
||
|
|
process.stdout.write(JSON.stringify({{removed, replaced}}));
|
||
|
|
"""
|
||
|
|
out = subprocess.run([NODE, "-e", script], capture_output=True, text=True, timeout=20)
|
||
|
|
assert out.returncode == 0, f"node failed: {out.stderr}"
|
||
|
|
return json.loads(out.stdout)
|
||
|
|
|
||
|
|
|
||
|
|
def test_clears_stale_session_on_boot_failure():
|
||
|
|
"""No active session (boot) + a failed load → clear the stale id so the next
|
||
|
|
boot doesn't retry the dead session."""
|
||
|
|
data = _run_helper("null")
|
||
|
|
assert data["removed"] is True
|
||
|
|
assert data["replaced"] is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_does_not_clear_when_viewing_a_healthy_session():
|
||
|
|
"""An active session on screen (currentSid set) → do NOT wipe localStorage/URL;
|
||
|
|
the failure may be transient and the live session must survive (#2782/#4028)."""
|
||
|
|
data = _run_helper("'live-session-123'")
|
||
|
|
assert data["removed"] is False
|
||
|
|
assert data["replaced"] is False
|