From 127a57e0c5f8c664209cbf5ce0c4b4ec9735b79c Mon Sep 17 00:00:00 2001 From: Anthony Ruiz Date: Sun, 26 Jul 2026 11:40:49 +0000 Subject: [PATCH] fix(sessions): fully consume PWA launch action --- static/sessions.js | 15 +- tests/test_hard_refresh_session_restore.py | 173 ++++++++++++++++++++- 2 files changed, 178 insertions(+), 10 deletions(-) diff --git a/static/sessions.js b/static/sessions.js index 55f96181..d2de922e 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -4151,11 +4151,14 @@ 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'); - base.search=current.searchParams.toString(); + const retained=new URLSearchParams(); + current.searchParams.forEach((value,key)=>{ + if(key!=='action'||value!=='new-chat') retained.append(key,value); + }); + base.search=retained.toString(); base.hash=current.hash; }catch(_e){} return base.pathname+base.search+base.hash; @@ -4164,7 +4167,13 @@ function _setActiveSessionUrl(sid){ if(typeof window==='undefined'||!window.history||!sid) return; const next=_sessionUrlForSid(sid); if(next && next!==(window.location.pathname+window.location.search+window.location.hash)){ - window.history.pushState({session_id:sid},'',next); + let consumeLaunchAction=false; + try{ + const current=new URL(window.location.href); + consumeLaunchAction=current.searchParams.getAll('action').includes('new-chat'); + }catch(_e){} + const method=consumeLaunchAction?'replaceState':'pushState'; + window.history[method]({session_id:sid},'',next); } } diff --git a/tests/test_hard_refresh_session_restore.py b/tests/test_hard_refresh_session_restore.py index cb1aa060..c39a53d2 100644 --- a/tests/test_hard_refresh_session_restore.py +++ b/tests/test_hard_refresh_session_restore.py @@ -31,7 +31,7 @@ def _run_node(source: str) -> dict: return json.loads(result.stdout) -def test_new_chat_launch_action_is_consumed_before_hard_refresh(): +def test_only_exact_new_chat_launch_action_is_consumed_before_hard_refresh(): source = f""" const sessionsSrc = {SESSIONS_JS!r}; function extractFunc(src, name) {{ @@ -56,22 +56,22 @@ function applyUrl(rel) {{ }} global.window = {{ location: {{}} }}; global.document = {{ baseURI: 'https://example.test/app/' }}; -applyUrl('/app/?action=new-chat&keep=1#frag'); +applyUrl('/app/?action=new-chat&action=continue&keep=1&keep=2#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'), + launchActions: promotedUrl.searchParams.getAll('action'), + keep: promotedUrl.searchParams.getAll('keep'), }})); """ payload = _run_node(source) assert payload == { - "promoted": "/app/session/session-123?keep=1#frag", - "launchAction": None, - "keep": "1", + "promoted": "/app/session/session-123?action=continue&keep=1&keep=2#frag", + "launchActions": ["continue"], + "keep": ["1", "2"], } @@ -108,3 +108,162 @@ console.log(JSON.stringify({{ "ordinarySessionLoad": False, } assert "if(_shouldStartFreshPwaChat(pwaLaunchAction,urlSession)){" in BOOT_JS + + +def test_root_pwa_launch_replaces_source_history_before_boot_reentry(): + source = f""" +const sessionsSrc = {SESSIONS_JS!r}; +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); +}} +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; +}} +const entries = ['/app/?source=pwa&action=new-chat']; +let index = 0; +global.window = {{ + location: {{}}, + history: {{ + state: null, + pushState(state, _title, url) {{ + entries.splice(index + 1); + entries.push(url); + index++; + this.state = state; + applyUrl(url); + }}, + replaceState(state, _title, url) {{ + entries[index] = url; + this.state = state; + applyUrl(url); + }}, + }}, +}}; +global.document = {{ baseURI: 'https://example.test/app/' }}; +applyUrl(entries[0]); +globalThis._sessionUrlForSid = (0, eval)('(' + extractFunc(sessionsSrc, '_sessionUrlForSid') + ')'); +globalThis._setActiveSessionUrl = (0, eval)('(' + extractFunc(sessionsSrc, '_setActiveSessionUrl') + ')'); +globalThis._shouldStartFreshPwaChat = (0, eval)('(' + extractFunc(bootSrc, '_shouldStartFreshPwaChat') + ')'); +let newSessionCalls = 0; +let loadSessionCalls = 0; +function simulatedBoot() {{ + const url = new URL(window.location.href); + const explicitSid = url.pathname.split('/session/')[1] || url.searchParams.get('session'); + if (_shouldStartFreshPwaChat(url.searchParams.get('action'), explicitSid)) newSessionCalls++; + else if (explicitSid) loadSessionCalls++; +}} +newSessionCalls++; +_setActiveSessionUrl('session-123'); +const backTarget = entries[index - 1] || entries[index]; +applyUrl(backTarget); +simulatedBoot(); +console.log(JSON.stringify({{entries, index, newSessionCalls, loadSessionCalls, href: window.location.href}})); +""" + payload = _run_node(source) + + assert payload == { + "entries": ["/app/session/session-123?source=pwa"], + "index": 0, + "newSessionCalls": 1, + "loadSessionCalls": 1, + "href": "https://example.test/app/session/session-123?source=pwa", + } + + +@pytest.mark.parametrize( + ("initial_url", "expected_url"), + [ + ( + "/app/session/session-123?action=new-chat&action=continue&keep=1#frag", + "/app/session/session-123?action=continue&keep=1#frag", + ), + ( + "/app/?session=session-123&action=new-chat&action=continue&keep=1#frag", + "/app/session/session-123?action=continue&keep=1#frag", + ), + ], +) +def test_explicit_session_wins_and_replaces_stale_launch_history(initial_url, expected_url): + source = f""" +const sessionsSrc = {SESSIONS_JS!r}; +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); +}} +function applyUrl(rel) {{ + const next = new URL(rel, 'https://example.test'); + Object.assign(window.location, {{href: next.href, pathname: next.pathname, search: next.search, hash: next.hash}}); +}} +const calls = []; +global.window = {{location: {{}}, history: {{ + pushState(_state, _title, url) {{ calls.push(['push', url]); applyUrl(url); }}, + replaceState(_state, _title, url) {{ calls.push(['replace', url]); applyUrl(url); }}, +}}}}; +global.document = {{baseURI: 'https://example.test/app/'}}; +applyUrl({initial_url!r}); +globalThis._sessionUrlForSid = (0, eval)('(' + extractFunc(sessionsSrc, '_sessionUrlForSid') + ')'); +globalThis._setActiveSessionUrl = (0, eval)('(' + extractFunc(sessionsSrc, '_setActiveSessionUrl') + ')'); +globalThis._shouldStartFreshPwaChat = (0, eval)('(' + extractFunc(bootSrc, '_shouldStartFreshPwaChat') + ')'); +const before = new URL(window.location.href); +const explicitSid = before.pathname.split('/session/')[1] || before.searchParams.get('session'); +const shouldStartFresh = _shouldStartFreshPwaChat(before.searchParams.get('action'), explicitSid); +_setActiveSessionUrl(explicitSid); +console.log(JSON.stringify({{shouldStartFresh, calls, finalUrl: window.location.pathname + window.location.search + window.location.hash}})); +""" + payload = _run_node(source) + + assert payload == { + "shouldStartFresh": False, + "calls": [["replace", expected_url]], + "finalUrl": expected_url, + } + + +def test_ordinary_session_navigation_keeps_push_state_semantics(): + source = f""" +const sessionsSrc = {SESSIONS_JS!r}; +function extractFunc(src, name) {{ + const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\('); + const start = src.search(re); + let i = src.indexOf('{{', start), depth = 1; i++; + while (depth > 0) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} + return src.slice(start, i); +}} +global.window = {{ + location: {{href: 'https://example.test/app/?keep=1', pathname: '/app/', search: '?keep=1', hash: ''}}, + history: {{pushState(_state, _title, url) {{ console.log(JSON.stringify({{method: 'push', url}})); }}, replaceState() {{ throw new Error('unexpected replace'); }}}}, +}}; +global.document = {{baseURI: 'https://example.test/app/'}}; +globalThis._sessionUrlForSid = (0, eval)('(' + extractFunc(sessionsSrc, '_sessionUrlForSid') + ')'); +globalThis._setActiveSessionUrl = (0, eval)('(' + extractFunc(sessionsSrc, '_setActiveSessionUrl') + ')'); +_setActiveSessionUrl('session-123'); +""" + assert _run_node(source) == { + "method": "push", + "url": "/app/session/session-123?keep=1", + }