fix(desktop): guard gateway-open resume against active fresh-draft transition (#68594)
When switching profiles, useRouteResume() could treat the target profile gateway opening as a resume command for the old routed session before React Router commits the /new pathname. The gatewayBecameOpen trigger was not guarded by the existing freshDraftReady discriminator, unlike the stuckOnRoutedSession guard on line 137 which already uses it. Fix: add && !freshDraftReady to the gatewayBecameOpen condition in shouldResume (line 143). This is consistent with the existing pattern and preserves reconnect behavior (freshDraftReady=false during normal reconnects). Adds a regression test that simulates the exact race: profile switch clears refs and sets freshDraftReady=true, gateway closes, then profile B gateway opens while pathname is still /session-a. Asserts no resume fires.
This commit is contained in:
committed by
Teknium
parent
4b21f970aa
commit
2a4460b58b
@@ -389,6 +389,78 @@ describe('useRouteResume', () => {
|
||||
expect(resumeSession).toHaveBeenCalledTimes(1)
|
||||
expect(resumeSession).toHaveBeenCalledWith('session-2', true)
|
||||
})
|
||||
|
||||
it("does not re-resume the old session when the new profile gateway opens before /new commits (#68594)", () => {
|
||||
const resumeSession = vi.fn(async () => undefined)
|
||||
const startFreshSessionDraft = vi.fn()
|
||||
const activeSessionIdRef: MutableRefObject<null | string> = { current: "runtime-a" }
|
||||
const creatingSessionRef = { current: false }
|
||||
const runtimeIdByStoredSessionIdRef = { current: new Map([["session-a", "runtime-a"]]) }
|
||||
const selectedStoredSessionIdRef: MutableRefObject<null | string> = { current: "session-a" }
|
||||
|
||||
const { rerender } = render(
|
||||
<RouteResumeHarness
|
||||
activeSessionId="runtime-a"
|
||||
activeSessionIdRef={activeSessionIdRef}
|
||||
creatingSessionRef={creatingSessionRef}
|
||||
currentView="chat"
|
||||
freshDraftReady={false}
|
||||
gatewayState="open"
|
||||
locationPathname="/session-a"
|
||||
resumeSession={resumeSession}
|
||||
routedSessionId="session-a"
|
||||
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
|
||||
selectedStoredSessionId="session-a"
|
||||
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
|
||||
startFreshSessionDraft={startFreshSessionDraft}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(resumeSession).not.toHaveBeenCalled()
|
||||
|
||||
// Profile switch: clear refs, set freshDraftReady, close profile A gateway.
|
||||
activeSessionIdRef.current = null
|
||||
selectedStoredSessionIdRef.current = null
|
||||
rerender(
|
||||
<RouteResumeHarness
|
||||
activeSessionId={null}
|
||||
activeSessionIdRef={activeSessionIdRef}
|
||||
creatingSessionRef={creatingSessionRef}
|
||||
currentView="chat"
|
||||
freshDraftReady
|
||||
gatewayState="closed"
|
||||
locationPathname="/session-a"
|
||||
resumeSession={resumeSession}
|
||||
routedSessionId="session-a"
|
||||
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
|
||||
selectedStoredSessionId={null}
|
||||
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
|
||||
startFreshSessionDraft={startFreshSessionDraft}
|
||||
/>
|
||||
)
|
||||
|
||||
// Profile B gateway opens before React Router commits /new.
|
||||
rerender(
|
||||
<RouteResumeHarness
|
||||
activeSessionId={null}
|
||||
activeSessionIdRef={activeSessionIdRef}
|
||||
creatingSessionRef={creatingSessionRef}
|
||||
currentView="chat"
|
||||
freshDraftReady
|
||||
gatewayState="open"
|
||||
locationPathname="/session-a"
|
||||
resumeSession={resumeSession}
|
||||
routedSessionId="session-a"
|
||||
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
|
||||
selectedStoredSessionId={null}
|
||||
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
|
||||
startFreshSessionDraft={startFreshSessionDraft}
|
||||
/>
|
||||
)
|
||||
|
||||
// Must NOT resume session-a: the fresh draft transition is active.
|
||||
expect(resumeSession).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useRouteResume bounded auto-retry after a failed resume', () => {
|
||||
@@ -602,3 +674,4 @@ describe('useRouteResume bounded auto-retry after a failed resume', () => {
|
||||
expect($resumeExhaustedSessionId.get()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -156,7 +156,8 @@ export function useRouteResume({
|
||||
// we're stranded on a routed session that never loaded. The first two
|
||||
// guard against a transient /:sid re-resume during "new chat" state clears
|
||||
// before the pathname updates from /:sid -> /.
|
||||
const shouldResume = pathnameChanged || gatewayBecameOpen || stuckOnRoutedSession || explicitlyRequested
|
||||
const shouldResume =
|
||||
pathnameChanged || (gatewayBecameOpen && !freshDraftReady) || stuckOnRoutedSession || explicitlyRequested
|
||||
|
||||
// On a reconnect (gatewayBecameOpen) re-resume even when the route looks
|
||||
// `alreadyActive`: the cached runtime id can be stale once the gateway
|
||||
|
||||
Reference in New Issue
Block a user