fix(desktop): isolate explicit queued submit recovery

Signed-off-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
This commit is contained in:
Shawn Wang
2026-08-22 11:18:15 +08:00
committed by Teknium
parent 534719259c
commit 11fd82cf20
2 changed files with 73 additions and 1 deletions
@@ -4708,6 +4708,78 @@ describe('usePromptActions busy-gateway churn tolerance (#64327)', () => {
])
})
it('keeps an explicit background queue target isolated from foreground routed recovery (#90428)', async () => {
const FOREGROUND_STORED_ID = 'stored-foreground-b'
const FOREGROUND_RUNTIME_ID = 'rt-foreground-b'
const QUEUED_STORED_ID = 'stored-queued-c'
const QUEUED_RUNTIME_ID = 'rt-queued-c-recovered'
const calls: { method: string; params?: Record<string, unknown> }[] = []
const stateWrites: { sessionId: string; storedSessionId: null | string | undefined }[] = []
const selectedStoredSessionIdRef: MutableRefObject<string | null> = { current: FOREGROUND_STORED_ID }
const activeSessionIdRef: MutableRefObject<string | null> = { current: FOREGROUND_RUNTIME_ID }
const runtimeIdByStoredSessionIdRef: MutableRefObject<Map<string, string>> = {
current: new Map([[FOREGROUND_STORED_ID, FOREGROUND_RUNTIME_ID]])
}
const resumeStoredSession = vi.fn(async () => undefined)
const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
calls.push({ method, params })
if (method === 'session.resume') {
expect(params?.session_id).toBe(QUEUED_STORED_ID)
return { session_id: QUEUED_RUNTIME_ID } as never
}
return {} as never
})
let handle: HarnessHandle | null = null
render(
<Harness
activeSessionId={FOREGROUND_RUNTIME_ID}
activeSessionIdRef={activeSessionIdRef}
getRoutedStoredSessionId={() => FOREGROUND_STORED_ID}
getRouteToken={() => `/${FOREGROUND_STORED_ID}::`}
getRuntimeIdForStoredSession={storedId => runtimeIdByStoredSessionIdRef.current.get(storedId) ?? null}
onReady={h => (handle = h)}
onUpdateState={(sessionId, storedSessionId) => stateWrites.push({ sessionId, storedSessionId })}
refreshSessions={async () => undefined}
requestGateway={requestGateway}
resumeStoredSession={resumeStoredSession}
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
storedSessionId={FOREGROUND_STORED_ID}
/>
)
await waitFor(() => expect(handle).not.toBeNull())
expect(
await handle!.submitText('queued prompt for C', {
fromQueue: true,
sessionId: null,
storedSessionId: QUEUED_STORED_ID
})
).toBe(true)
expect(resumeStoredSession).not.toHaveBeenCalled()
expect(calls).toEqual([
{
method: 'session.resume',
params: { session_id: QUEUED_STORED_ID, source: 'desktop', omit_messages: true }
},
{
method: 'prompt.submit',
params: { session_id: QUEUED_RUNTIME_ID, text: 'queued prompt for C', queued: true }
}
])
expect(stateWrites.some(write => write.sessionId === FOREGROUND_RUNTIME_ID)).toBe(false)
expect(selectedStoredSessionIdRef.current).toBe(FOREGROUND_STORED_ID)
expect(activeSessionIdRef.current).toBe(FOREGROUND_RUNTIME_ID)
expect(runtimeIdByStoredSessionIdRef.current).toEqual(new Map([[FOREGROUND_STORED_ID, FOREGROUND_RUNTIME_ID]]))
})
it('still aborts when the user genuinely moves to a different chat mid-submit', async () => {
// The churn tolerance must not weaken the real guard: selection AND route
// moving to another actual chat is a user switch and must abort.
@@ -527,7 +527,7 @@ export function useSubmitPrompt(deps: SubmitPromptDeps) {
scope.setMessages(current => [...current, buildUserMessage()])
}
if (!sessionId && routedStoredSessionId && routedSessionNeedsResume) {
if (!options?.storedSessionId && !sessionId && routedStoredSessionId && routedSessionNeedsResume) {
// The URL still names a durable conversation, but a profile
// swap/reconnect left its volatile session binding incomplete or
// cross-wired. Run the full profile-aware resume path. Creating here