From cd04f5fcc2c9000f0d6bab5bcb1c5ce9c3a95e8b Mon Sep 17 00:00:00 2001 From: Anthony Ruiz Date: Sat, 25 Jul 2026 11:09:57 +0000 Subject: [PATCH] fix(sessions): preserve active chat across hard refresh --- static/boot.js | 5 +- static/sessions.js | 1 + tests/test_hard_refresh_session_restore.py | 110 ++++++++++++++++++ tests/test_new_chat_default_model_frontend.py | 2 +- tests/test_pwa_manifest_sw.py | 3 +- 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 tests/test_hard_refresh_session_restore.py diff --git a/static/boot.js b/static/boot.js index a45a33a4..e13d35de 100644 --- a/static/boot.js +++ b/static/boot.js @@ -132,6 +132,9 @@ function _rootPrefillNeedsFreshComposer(urlSession, savedLocal, prefillIntent){ function _profileQueryBlocksSavedLocalRestore(profileIntent, urlSession){ return !!(profileIntent&&profileIntent.hasParam&&profileIntent.valid&&!urlSession); } +function _shouldStartFreshPwaChat(action,urlSession){ + return action==='new-chat'&&!urlSession; +} async function _applyComposerPrefillOnBoot(prefillIntent){ if(!prefillIntent||!prefillIntent.hasText) return; const msg=(typeof $==='function')?$('msg'):document.getElementById('msg'); @@ -3676,7 +3679,7 @@ window._mirrorSpeechSettingsFromServer=_mirrorSpeechSettingsFromServer; const pwaLaunchAction=(window.HermesPWA&&typeof window.HermesPWA.launchAction==='function') ? window.HermesPWA.launchAction() : null; - if(pwaLaunchAction==='new-chat'){ + if(_shouldStartFreshPwaChat(pwaLaunchAction,urlSession)){ try{ await newSession(true); // New-chat PWA launches need the empty conversation visible immediately. diff --git a/static/sessions.js b/static/sessions.js index cfd24164..55f96181 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -4151,6 +4151,7 @@ function _sessionUrlForSid(sid){ const current=new URL(window.location.href); current.searchParams.delete('session'); current.searchParams.delete('session_id'); + current.searchParams.delete('action'); current.searchParams.delete('q'); current.searchParams.delete('prompt'); current.searchParams.delete('send'); diff --git a/tests/test_hard_refresh_session_restore.py b/tests/test_hard_refresh_session_restore.py new file mode 100644 index 00000000..cb1aa060 --- /dev/null +++ b/tests/test_hard_refresh_session_restore.py @@ -0,0 +1,110 @@ +"""Regression coverage for one-shot PWA launch actions on hard refresh.""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).parent.parent.resolve() +SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8") +BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8") +NODE = shutil.which("node") + +pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") + + +def _run_node(source: str) -> dict: + result = subprocess.run( + [NODE], + input=source, + cwd=str(REPO_ROOT), + capture_output=True, + encoding="utf-8", + text=True, + timeout=30, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr) + return json.loads(result.stdout) + + +def test_new_chat_launch_action_is_consumed_before_hard_refresh(): + source = f""" +const sessionsSrc = {SESSIONS_JS!r}; +function extractFunc(src, name) {{ + const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\('); + const start = src.search(re); + if (start < 0) throw new Error(name + ' not found'); + let i = src.indexOf('{{', start); + let depth = 1; i++; + while (depth > 0 && i < src.length) {{ + if (src[i] === '{{') depth++; + else if (src[i] === '}}') depth--; + i++; + }} + return src.slice(start, i); +}} +function applyUrl(rel) {{ + const next = new URL(rel, 'https://example.test'); + window.location.href = next.href; + window.location.pathname = next.pathname; + window.location.search = next.search; + window.location.hash = next.hash; +}} +global.window = {{ location: {{}} }}; +global.document = {{ baseURI: 'https://example.test/app/' }}; +applyUrl('/app/?action=new-chat&keep=1#frag'); +globalThis._sessionUrlForSid = (0, eval)('(' + extractFunc(sessionsSrc, '_sessionUrlForSid') + ')'); +const promoted = _sessionUrlForSid('session-123'); +const promotedUrl = new URL(promoted, 'https://example.test'); +console.log(JSON.stringify({{ + promoted, + launchAction: promotedUrl.searchParams.get('action'), + keep: promotedUrl.searchParams.get('keep'), +}})); +""" + payload = _run_node(source) + + assert payload == { + "promoted": "/app/session/session-123?keep=1#frag", + "launchAction": None, + "keep": "1", + } + + +def test_stale_new_chat_action_does_not_override_explicit_session_url(): + source = f""" +const bootSrc = {BOOT_JS!r}; +function extractFunc(src, name) {{ + const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\('); + const start = src.search(re); + if (start < 0) throw new Error(name + ' not found'); + let i = src.indexOf('{{', start); + let depth = 1; i++; + while (depth > 0 && i < src.length) {{ + if (src[i] === '{{') depth++; + else if (src[i] === '}}') depth--; + i++; + }} + return src.slice(start, i); +}} +globalThis._shouldStartFreshPwaChat = (0, eval)( + '(' + extractFunc(bootSrc, '_shouldStartFreshPwaChat') + ')' +); +console.log(JSON.stringify({{ + freshRootLaunch: _shouldStartFreshPwaChat('new-chat', null), + staleSessionLaunch: _shouldStartFreshPwaChat('new-chat', 'session-123'), + ordinarySessionLoad: _shouldStartFreshPwaChat(null, 'session-123'), +}})); +""" + payload = _run_node(source) + + assert payload == { + "freshRootLaunch": True, + "staleSessionLaunch": False, + "ordinarySessionLoad": False, + } + assert "if(_shouldStartFreshPwaChat(pwaLaunchAction,urlSession)){" in BOOT_JS diff --git a/tests/test_new_chat_default_model_frontend.py b/tests/test_new_chat_default_model_frontend.py index e46ffb65..56288ac8 100644 --- a/tests/test_new_chat_default_model_frontend.py +++ b/tests/test_new_chat_default_model_frontend.py @@ -71,7 +71,7 @@ def test_hard_refresh_hydrates_saved_session_model_before_revealing_model_chip() def test_pwa_new_chat_launch_does_not_block_first_paint_on_model_catalog(): boot_js = Path("static/boot.js").read_text(encoding="utf-8") - launch_marker = "if(pwaLaunchAction==='new-chat'){" + launch_marker = "if(_shouldStartFreshPwaChat(pwaLaunchAction,urlSession)){" assert launch_marker in boot_js launch_branch = boot_js[boot_js.index(launch_marker) : boot_js.index("const savedLocal=localStorage.getItem", boot_js.index(launch_marker))] assert "await newSession(true);" in launch_branch diff --git a/tests/test_pwa_manifest_sw.py b/tests/test_pwa_manifest_sw.py index 380e110f..0f537d2b 100644 --- a/tests/test_pwa_manifest_sw.py +++ b/tests/test_pwa_manifest_sw.py @@ -339,7 +339,8 @@ class TestIndexHtmlIntegration: src = BOOT.read_text(encoding="utf-8") assert "pwaLaunchAction" in src assert "launchAction()" in src - assert "pwaLaunchAction==='new-chat'" in src + assert "function _shouldStartFreshPwaChat(action,urlSession)" in src + assert "_shouldStartFreshPwaChat(pwaLaunchAction,urlSession)" in src assert "await newSession(true)" in src def test_index_route_url_encodes_asset_version(self):