Files
aiturk-hermes-ide/apps/desktop/src/store/session-watchdog.test.ts
T

262 lines
8.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ClientSessionState } from '@/app/types'
import { createClientSessionState } from '@/lib/chat-runtime'
import { $activeSessionId, $selectedStoredSessionId, $unreadFinishedSessionIds } from './session'
import {
$attentionSessionIds,
$stalledSessionIds,
$workingSessionIds,
clearAllSessionStates,
getRecentlySettledSessionIds,
publishSessionState,
SESSION_WATCHDOG_TIMEOUT_MS
} from './session-states'
// Read from the store rather than restated here: these assert what happens on
// either side of the threshold, not what the threshold is.
const WATCHDOG_MS = SESSION_WATCHDOG_TIMEOUT_MS
function state(over: Partial<ClientSessionState> = {}): ClientSessionState {
return { ...createClientSessionState(null), storedSessionId: 's1', ...over }
}
describe('session status transitions', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(0)
clearAllSessionStates()
$unreadFinishedSessionIds.set([])
$selectedStoredSessionId.set(null)
$activeSessionId.set(null)
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
clearAllSessionStates()
$unreadFinishedSessionIds.set([])
$selectedStoredSessionId.set(null)
$activeSessionId.set(null)
})
it('adds a session to $workingSessionIds when busy transitions to true', () => {
const s = state({ busy: false, storedSessionId: 's1' })
publishSessionState('rt1', s)
publishSessionState('rt1', { ...s, busy: true })
expect($workingSessionIds.get()).toContain('s1')
})
it('removes a session from $workingSessionIds when busy transitions to false', () => {
const working = state({ busy: true, storedSessionId: 's1' })
publishSessionState('rt1', working)
expect($workingSessionIds.get()).toContain('s1')
publishSessionState('rt1', { ...working, busy: false })
expect($workingSessionIds.get()).not.toContain('s1')
})
it('adds a session to $attentionSessionIds when needsInput is true', () => {
const s = state({ busy: true, needsInput: false, storedSessionId: 's1' })
publishSessionState('rt1', s)
publishSessionState('rt1', { ...s, needsInput: true })
expect($attentionSessionIds.get()).toContain('s1')
})
it('marks a background session unread when its turn finishes', () => {
$selectedStoredSessionId.set('other-session')
const working = state({ busy: true, storedSessionId: 's1' })
publishSessionState('rt1', working)
publishSessionState('rt1', { ...working, busy: false })
expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
})
it('does NOT mark unread when the finishing session is the active one', () => {
$selectedStoredSessionId.set('s1')
const working = state({ busy: true, storedSessionId: 's1' })
publishSessionState('rt1', working)
publishSessionState('rt1', { ...working, busy: false })
expect($unreadFinishedSessionIds.get()).toEqual([])
})
it('does NOT mark unread on idle→idle re-asserts (no prior working state)', () => {
$selectedStoredSessionId.set('other-session')
publishSessionState('rt1', state({ busy: false, storedSessionId: 's1' }))
expect($unreadFinishedSessionIds.get()).toEqual([])
})
it('grants settle grace when a working session goes idle', () => {
$selectedStoredSessionId.set('other')
const working = state({ busy: true, storedSessionId: 's1' })
publishSessionState('rt1', working)
publishSessionState('rt1', { ...working, busy: false })
expect(getRecentlySettledSessionIds()).toEqual(['s1'])
})
it('does not grant grace on idle→idle re-asserts', () => {
publishSessionState('rt1', state({ busy: false, storedSessionId: 's1' }))
expect(getRecentlySettledSessionIds()).toEqual([])
})
it('clears settle grace when the session goes busy again', () => {
$selectedStoredSessionId.set('other')
const working = state({ busy: true, storedSessionId: 's2' })
publishSessionState('rt1', working)
const idle = { ...working, busy: false }
publishSessionState('rt1', idle)
expect(getRecentlySettledSessionIds()).toEqual(['s2'])
publishSessionState('rt1', { ...idle, busy: true })
expect(getRecentlySettledSessionIds()).toEqual([])
})
})
describe('session watchdog', () => {
beforeEach(() => {
vi.useFakeTimers()
clearAllSessionStates()
$unreadFinishedSessionIds.set([])
$selectedStoredSessionId.set(null)
$activeSessionId.set(null)
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
clearAllSessionStates()
$unreadFinishedSessionIds.set([])
$selectedStoredSessionId.set(null)
$activeSessionId.set(null)
})
it('marks a silent session stalled without pretending it finished', () => {
publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
vi.advanceTimersByTime(WATCHDOG_MS)
expect($workingSessionIds.get()).toContain('s1')
expect($stalledSessionIds.get()).toContain('s1')
})
it('clears stalled on new activity and rearms the watchdog', () => {
const working = state({ busy: true, storedSessionId: 's2' })
publishSessionState('rt2', working)
vi.advanceTimersByTime(WATCHDOG_MS)
expect($stalledSessionIds.get()).toContain('s2')
publishSessionState('rt2', { ...working, awaitingResponse: true })
expect($stalledSessionIds.get()).not.toContain('s2')
vi.advanceTimersByTime(WATCHDOG_MS - 1)
expect($stalledSessionIds.get()).not.toContain('s2')
expect($workingSessionIds.get()).toContain('s2')
})
it('clears both running and stalled on an authoritative terminal transition', () => {
const working = state({ busy: true, storedSessionId: 's3' })
publishSessionState('rt3', working)
vi.advanceTimersByTime(WATCHDOG_MS)
expect($stalledSessionIds.get()).toContain('s3')
publishSessionState('rt3', { ...working, busy: false })
expect($workingSessionIds.get()).not.toContain('s3')
expect($stalledSessionIds.get()).not.toContain('s3')
})
it('never marks a session stalled when it settles before the window', () => {
const working = state({ busy: true, storedSessionId: 's4' })
publishSessionState('rt4', working)
publishSessionState('rt4', { ...working, busy: false })
vi.advanceTimersByTime(WATCHDOG_MS)
expect($workingSessionIds.get()).not.toContain('s4')
expect($stalledSessionIds.get()).not.toContain('s4')
})
it('clears stalled state and disarms timers on a gateway wipe', () => {
publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
vi.advanceTimersByTime(WATCHDOG_MS)
expect($stalledSessionIds.get()).toEqual(['s1'])
clearAllSessionStates()
vi.advanceTimersByTime(WATCHDOG_MS)
expect($workingSessionIds.get()).toEqual([])
expect($stalledSessionIds.get()).toEqual([])
})
})
describe('computed $workingSessionIds', () => {
beforeEach(() => {
clearAllSessionStates()
})
afterEach(() => {
clearAllSessionStates()
})
it('is empty when no sessions are busy', () => {
expect($workingSessionIds.get()).toEqual([])
})
it('reflects busy sessions under the id their surfaces key on', () => {
publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
publishSessionState('rt2', state({ busy: false, storedSessionId: 's2' }))
// Not yet persisted, so the runtime id is the only id it has — and the one
// the row is keyed by until the backend hands a stored id back.
publishSessionState('rt3', state({ busy: true, storedSessionId: null }))
expect($workingSessionIds.get()).toEqual(['s1', 'rt3'])
})
it('updates when session state changes', () => {
publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
expect($workingSessionIds.get()).toEqual(['s1'])
publishSessionState('rt1', state({ busy: false, storedSessionId: 's1' }))
expect($workingSessionIds.get()).toEqual([])
})
})
describe('computed $attentionSessionIds', () => {
beforeEach(() => {
clearAllSessionStates()
})
afterEach(() => {
clearAllSessionStates()
})
it('reflects sessions with needsInput=true and a storedSessionId', () => {
publishSessionState('rt1', state({ needsInput: true, storedSessionId: 's1' }))
publishSessionState('rt2', state({ needsInput: false, storedSessionId: 's2' }))
expect($attentionSessionIds.get()).toEqual(['s1'])
})
it('clears when $sessionStates is cleared', () => {
publishSessionState('rt1', state({ needsInput: true, storedSessionId: 's1' }))
expect($attentionSessionIds.get()).toEqual(['s1'])
clearAllSessionStates()
expect($attentionSessionIds.get()).toEqual([])
})
})