137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { createClientSessionState } from '@/lib/chat-runtime'
|
|
import { $activeSessionId, $selectedStoredSessionId, $unreadFinishedSessionIds } from '@/store/session'
|
|
import {
|
|
$attentionSessionIds,
|
|
$sessionStates,
|
|
$workingSessionIds,
|
|
clearAllSessionStates,
|
|
publishSessionState
|
|
} from '@/store/session-states'
|
|
|
|
import { rehydrateLiveSessionStatuses } from './use-background-sync'
|
|
|
|
/**
|
|
* `session.active_list` is the authoritative snapshot of what is RUNNING in the
|
|
* polled gateway process. A session that finished while Desktop was looking
|
|
* elsewhere — or whose runtime id was recycled by a backend respawn — simply
|
|
* stops appearing in the response. Absence is therefore a completion signal,
|
|
* not "no news": if nothing reaps it, the row spins forever and the
|
|
* busy→idle edge that paints the green "your turn" dot never fires.
|
|
*/
|
|
describe('rehydrateLiveSessionStatuses — reaping vanished runtimes', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
$selectedStoredSessionId.set(null)
|
|
$unreadFinishedSessionIds.set([])
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllTimers()
|
|
vi.useRealTimers()
|
|
clearAllSessionStates()
|
|
$unreadFinishedSessionIds.set([])
|
|
$activeSessionId.set(null)
|
|
})
|
|
|
|
it('clears a working session that disappears from the live snapshot', () => {
|
|
rehydrateLiveSessionStatuses({
|
|
sessions: [{ id: 'runtime-a', session_key: 'stored-a', status: 'working' }]
|
|
})
|
|
|
|
expect($workingSessionIds.get()).toEqual(['stored-a'])
|
|
|
|
// The turn finished and the gateway reaped the session between polls.
|
|
rehydrateLiveSessionStatuses({ sessions: [] })
|
|
|
|
expect($workingSessionIds.get()).toEqual([])
|
|
})
|
|
|
|
it('fires the unread "your turn" marker for a vanished background session', () => {
|
|
rehydrateLiveSessionStatuses({
|
|
sessions: [{ id: 'runtime-b', session_key: 'stored-b', status: 'working' }]
|
|
})
|
|
|
|
rehydrateLiveSessionStatuses({ sessions: [] })
|
|
|
|
expect($unreadFinishedSessionIds.get()).toEqual(['stored-b'])
|
|
})
|
|
|
|
it('clears a blocked session that disappears from the live snapshot', () => {
|
|
rehydrateLiveSessionStatuses({
|
|
sessions: [{ id: 'runtime-c', session_key: 'stored-c', status: 'waiting' }]
|
|
})
|
|
|
|
expect($attentionSessionIds.get()).toEqual(['stored-c'])
|
|
|
|
rehydrateLiveSessionStatuses({ sessions: [] })
|
|
|
|
expect($attentionSessionIds.get()).toEqual([])
|
|
})
|
|
|
|
it('leaves runtimes this poll never seeded alone', () => {
|
|
// A background PROFILE's sessions are served by a different gateway and
|
|
// never appear in this profile's active_list. Reaping them would dark out
|
|
// every other profile's running rows.
|
|
rehydrateLiveSessionStatuses(
|
|
{ sessions: [{ id: 'runtime-other', session_key: 'stored-other', status: 'working' }] },
|
|
Date.now(),
|
|
'other'
|
|
)
|
|
|
|
rehydrateLiveSessionStatuses({ sessions: [] }, Date.now(), 'default')
|
|
|
|
expect($workingSessionIds.get()).toEqual(['stored-other'])
|
|
})
|
|
|
|
it('seals open tool parts and clears awaitingResponse when a session vanishes', () => {
|
|
const openTool = {
|
|
type: 'tool-call',
|
|
toolCallId: 'call-1',
|
|
toolName: 'patch',
|
|
args: {},
|
|
argsText: '{}'
|
|
} as never
|
|
|
|
publishSessionState('runtime-tools', {
|
|
...createClientSessionState('stored-tools'),
|
|
busy: true,
|
|
awaitingResponse: true,
|
|
messages: [{ id: 'a1', role: 'assistant', parts: [openTool], pending: false } as never]
|
|
})
|
|
|
|
// Keep the runtime referenced so the settled state stays in the store
|
|
// instead of being evicted as no-longer-needed.
|
|
$activeSessionId.set('runtime-tools')
|
|
|
|
rehydrateLiveSessionStatuses({
|
|
sessions: [{ id: 'runtime-tools', session_key: 'stored-tools', status: 'working' }]
|
|
})
|
|
rehydrateLiveSessionStatuses({ sessions: [] })
|
|
|
|
const state = $sessionStates.get()['runtime-tools']
|
|
|
|
expect(state.busy).toBe(false)
|
|
expect(state.awaitingResponse).toBe(false)
|
|
expect((state.messages[0].parts[0] as { result?: unknown }).result).toBeDefined()
|
|
})
|
|
|
|
it('clears a session stuck awaiting a response without the busy flag', () => {
|
|
publishSessionState('runtime-await', {
|
|
...createClientSessionState('stored-await'),
|
|
awaitingResponse: true,
|
|
busy: false
|
|
})
|
|
|
|
$activeSessionId.set('runtime-await')
|
|
|
|
rehydrateLiveSessionStatuses({
|
|
sessions: [{ id: 'runtime-await', session_key: 'stored-await', status: 'working' }]
|
|
})
|
|
rehydrateLiveSessionStatuses({ sessions: [] })
|
|
|
|
expect($sessionStates.get()['runtime-await'].awaitingResponse).toBe(false)
|
|
})
|
|
})
|