fix: restore inflight session on bfcache pageshow
This commit is contained in:
@@ -1357,7 +1357,7 @@ function applyBotName(){
|
||||
// sync whenever the page is restored from cache (`event.persisted === true`).
|
||||
// Fix #1045: also re-run topbar/workspace/panel state so the rail and layout
|
||||
// chrome aren't left in the stale bfcache snapshot.
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
window.addEventListener('pageshow', async (event) => {
|
||||
if (!event.persisted) return; // fresh loads are handled by the IIFE above
|
||||
const _srch = document.getElementById('sessionSearch');
|
||||
if (_srch) _srch.value = '';
|
||||
@@ -1367,6 +1367,17 @@ window.addEventListener('pageshow', (event) => {
|
||||
if (typeof closeReasoningDropdown === 'function') try { closeReasoningDropdown(); } catch (_) {}
|
||||
if (typeof closeWsDropdown === 'function') try { closeWsDropdown(); } catch (_) {}
|
||||
if (typeof closeProfileDropdown === 'function') try { closeProfileDropdown(); } catch (_) {}
|
||||
// BFCache restores the frozen DOM without rerunning boot. Refresh the active
|
||||
// session through the normal load path so in-flight sessions with
|
||||
// active_stream_id / pending_user_message can reattach like a reload restore.
|
||||
if (S.session && S.session.session_id && typeof loadSession === 'function') {
|
||||
try {
|
||||
await loadSession(S.session.session_id);
|
||||
if (S.session && S.session.session_id && typeof checkInflightOnBoot === 'function') {
|
||||
try { await checkInflightOnBoot(S.session.session_id); } catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// Re-synchronise layout chrome that the boot IIFE sets up but bfcache
|
||||
// doesn't re-run. Each call is guarded so missing helpers degrade silently.
|
||||
if (typeof syncTopbar === 'function') try { syncTopbar(); } catch (_) {}
|
||||
|
||||
@@ -20,14 +20,30 @@ def _boot_js() -> str:
|
||||
return (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _pageshow_handler(src: str) -> str:
|
||||
marker = "window.addEventListener('pageshow'"
|
||||
ps_idx = src.find(marker)
|
||||
assert ps_idx != -1, "pageshow listener not found in boot.js"
|
||||
brace = src.find("=> {", ps_idx)
|
||||
assert brace != -1, "pageshow listener body not found"
|
||||
brace = src.find("{", brace)
|
||||
depth = 1
|
||||
i = brace + 1
|
||||
while i < len(src) and depth:
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
assert depth == 0, "pageshow listener body did not close"
|
||||
return src[brace + 1 : i - 1]
|
||||
|
||||
|
||||
class TestBfcacheLayoutRestore:
|
||||
def test_pageshow_calls_sync_topbar(self):
|
||||
"""pageshow handler must call syncTopbar() on bfcache restore."""
|
||||
src = _boot_js()
|
||||
# Find the pageshow listener block
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
assert ps_idx != -1, "pageshow listener not found in boot.js"
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
assert "syncTopbar" in handler_body, (
|
||||
"pageshow handler must call syncTopbar() to restore topbar state after bfcache"
|
||||
)
|
||||
@@ -35,8 +51,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_pageshow_calls_sync_workspace_panel_state(self):
|
||||
"""pageshow handler must call syncWorkspacePanelState()."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
assert "syncWorkspacePanelState" in handler_body, (
|
||||
"pageshow handler must call syncWorkspacePanelState() on bfcache restore"
|
||||
)
|
||||
@@ -45,8 +60,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_pageshow_calls_start_gateway_sse(self):
|
||||
"""pageshow handler must call startGatewaySSE() to reconnect the dead SSE connection."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
assert "startGatewaySSE" in handler_body, (
|
||||
"pageshow handler must restart gateway SSE (bfcache-persisted connections are dead)"
|
||||
)
|
||||
@@ -54,8 +68,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_pageshow_still_clears_session_search(self):
|
||||
"""pageshow handler must still clear #sessionSearch (original #822 fix preserved)."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
assert "sessionSearch" in handler_body, (
|
||||
"pageshow handler must still clear #sessionSearch (regression: #822 fix must be preserved)"
|
||||
)
|
||||
@@ -63,8 +76,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_pageshow_still_calls_render_session_list_from_cache(self):
|
||||
"""pageshow handler must still call renderSessionListFromCache()."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
assert "renderSessionListFromCache" in handler_body, (
|
||||
"pageshow handler must still call renderSessionListFromCache() (regression: #822 fix)"
|
||||
)
|
||||
@@ -73,8 +85,7 @@ class TestBfcacheLayoutRestore:
|
||||
"""pageshow handler must NOT call _initResizePanels() — bfcache
|
||||
preserves event listeners so re-attaching them stacks duplicates."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
assert "_initResizePanels" not in handler_body, (
|
||||
"pageshow handler must not call _initResizePanels() — it stacks "
|
||||
"duplicate mousedown listeners on every bfcache restore"
|
||||
@@ -83,8 +94,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_new_calls_are_guarded_with_typeof(self):
|
||||
"""New calls in the pageshow handler must be typeof-guarded for safe degradation."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
# Each of the new calls must be guarded
|
||||
for fn in ("syncTopbar", "syncWorkspacePanelState", "startGatewaySSE",
|
||||
"closeModelDropdown", "closeReasoningDropdown", "closeWsDropdown", "closeProfileDropdown"):
|
||||
@@ -95,8 +105,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_pageshow_closes_all_dropdowns(self):
|
||||
"""pageshow handler must close all known dropdowns to reset frozen bfcache popover state."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
for fn in ("closeModelDropdown", "closeReasoningDropdown", "closeWsDropdown", "closeProfileDropdown"):
|
||||
assert fn in handler_body, (
|
||||
f"pageshow handler must call {fn}() to dismiss any dropdown left open by bfcache"
|
||||
@@ -105,8 +114,7 @@ class TestBfcacheLayoutRestore:
|
||||
def test_dropdowns_closed_before_layout_sync(self):
|
||||
"""Dropdown closes must come before layout sync calls (clean state first)."""
|
||||
src = _boot_js()
|
||||
ps_idx = src.find("window.addEventListener('pageshow'")
|
||||
handler_body = src[ps_idx:ps_idx + 1600]
|
||||
handler_body = _pageshow_handler(src)
|
||||
close_idx = handler_body.find("closeModelDropdown")
|
||||
sync_idx = handler_body.find("syncTopbar")
|
||||
assert close_idx != -1 and sync_idx != -1, "Both close and sync calls must be present"
|
||||
|
||||
66
tests/test_1466_bfcache_inflight_reattach.py
Normal file
66
tests/test_1466_bfcache_inflight_reattach.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Regression coverage for BFCache restore of an in-flight session (#1466).
|
||||
|
||||
A browser `pageshow` restore from BFCache does not re-run the boot IIFE. After
|
||||
#1473, normal reload restores in-flight sessions through `loadSession()` and
|
||||
`checkInflightOnBoot()`. BFCache restore should align with that path for the
|
||||
currently viewed session instead of only refreshing layout chrome/sidebar cache.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _pageshow_handler() -> str:
|
||||
marker = "window.addEventListener('pageshow'"
|
||||
start = BOOT_JS.find(marker)
|
||||
assert start != -1, "pageshow listener not found in boot.js"
|
||||
brace = BOOT_JS.find("=> {", start)
|
||||
assert brace != -1, "pageshow listener body not found"
|
||||
brace = BOOT_JS.find("{", brace)
|
||||
depth = 1
|
||||
i = brace + 1
|
||||
while i < len(BOOT_JS) and depth:
|
||||
if BOOT_JS[i] == "{":
|
||||
depth += 1
|
||||
elif BOOT_JS[i] == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
assert depth == 0, "pageshow listener body did not close"
|
||||
return BOOT_JS[brace + 1 : i - 1]
|
||||
|
||||
|
||||
def test_pageshow_restores_active_inflight_session_through_load_session():
|
||||
"""BFCache restore should refresh active session metadata and reattach stream.
|
||||
|
||||
The normal boot/reload path now calls loadSession(saved), which handles
|
||||
active_stream_id / pending_user_message and reattaches via attachLiveStream().
|
||||
The BFCache pageshow path must call the same loadSession path for the current
|
||||
active session because the boot IIFE does not run again.
|
||||
"""
|
||||
body = _pageshow_handler()
|
||||
assert "S.session" in body and "S.session.session_id" in body
|
||||
assert "loadSession(S.session.session_id" in body
|
||||
|
||||
|
||||
def test_pageshow_checks_persisted_inflight_marker_after_restoring_session():
|
||||
"""BFCache restore should preserve the reload recovery marker behavior.
|
||||
|
||||
If localStorage still has an inflight marker, the pageshow path should reuse
|
||||
checkInflightOnBoot() after restoring the active session, matching the normal
|
||||
reload path without adding new server-side persistence behavior.
|
||||
"""
|
||||
body = _pageshow_handler()
|
||||
load_pos = body.find("loadSession(S.session.session_id")
|
||||
check_pos = body.find("checkInflightOnBoot(S.session.session_id")
|
||||
assert load_pos != -1
|
||||
assert check_pos != -1
|
||||
assert load_pos < check_pos
|
||||
|
||||
|
||||
def test_pageshow_active_session_restore_is_guarded_and_non_blocking():
|
||||
"""pageshow handler must degrade safely when optional helpers are absent."""
|
||||
body = _pageshow_handler()
|
||||
assert "typeof loadSession === 'function'" in body
|
||||
assert "typeof checkInflightOnBoot === 'function'" in body
|
||||
assert "catch" in body[body.find("loadSession(S.session.session_id") : body.find("startGatewaySSE")]
|
||||
Reference in New Issue
Block a user