Files
hermes-web-ui/tests/client/chat-store-session-order.test.ts
2026-07-02 14:36:25 +08:00

142 lines
4.4 KiB
TypeScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useChatStore } from '@/stores/hermes/chat'
import { archiveSession, fetchSessions } from '@/api/hermes/sessions'
vi.mock('@/api/hermes/sessions', () => ({
archiveSession: vi.fn(),
fetchSessions: vi.fn(),
fetchSessionMessagesPage: vi.fn(),
fetchWorkspaceRunChangesForSession: vi.fn(async () => []),
fetchWorkspaceRunChangeFile: vi.fn(async () => null),
deleteSession: vi.fn(),
setSessionModel: vi.fn(),
}))
vi.mock('@/api/hermes/chat', () => ({
startRunViaSocket: vi.fn(),
resumeSession: vi.fn((_sessionId: string, cb: (data: any) => void) => {
cb({ session_id: _sessionId, isWorking: false, messages: [] })
}),
registerSessionHandlers: vi.fn(),
unregisterSessionHandlers: vi.fn(),
getChatRunSocket: vi.fn(() => ({ emit: vi.fn() })),
respondToolApproval: vi.fn(),
respondClarify: vi.fn(),
onPeerUserMessage: vi.fn(() => vi.fn()),
onSessionCommand: vi.fn(() => vi.fn()),
onSessionTitleUpdated: vi.fn(() => vi.fn()),
}))
vi.mock('@/api/client', () => ({
getActiveProfileName: () => 'default',
}))
vi.mock('@/api/hermes/download', () => ({
getDownloadUrl: (_path: string, name: string) => `/download/${name}`,
}))
vi.mock('@/utils/completion-sound', () => ({
primeCompletionSound: vi.fn(),
playCompletionSound: vi.fn(),
}))
vi.mock('@/utils/completion-notification', () => ({
showCompletionNotification: vi.fn(),
}))
vi.mock('@/utils/session-sync', () => ({
subscribeSessionSync: vi.fn(() => vi.fn()),
publishSessionSync: vi.fn(),
}))
function makeSession(id: string, times: Partial<{ started_at: number; ended_at: number | null; last_active: number }>) {
return {
id,
profile: 'default',
source: 'cli',
title: id,
preview: '',
started_at: times.started_at ?? 1,
ended_at: times.ended_at ?? null,
last_active: times.last_active,
message_count: 0,
tool_call_count: 0,
input_tokens: 0,
output_tokens: 0,
model: 'gpt-test',
provider: 'test',
}
}
describe('chat session ordering', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
setActivePinia(createPinia())
})
it('orders sessions by the newest known activity timestamp, including started_at', async () => {
vi.mocked(fetchSessions)
.mockResolvedValueOnce([
makeSession('older-active-session', { started_at: 100, last_active: 900 }),
makeSession('new-session-with-stale-last-active', { started_at: 1000, last_active: 200 }),
] as any)
.mockResolvedValueOnce([])
const store = useChatStore()
await store.loadSessions()
expect(store.sessions.map(session => session.id)).toEqual([
'new-session-with-stale-last-active',
'older-active-session',
])
expect(store.sessions[0].updatedAt).toBe(1000_000)
})
it('removes an archived session from the runtime session list', async () => {
vi.mocked(fetchSessions)
.mockResolvedValueOnce([
makeSession('active-session', { started_at: 1000, last_active: 1000 }),
makeSession('archive-me', { started_at: 900, last_active: 900 }),
] as any)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
makeSession('active-session', { started_at: 1000, last_active: 1000 }),
] as any)
.mockResolvedValueOnce([])
vi.mocked(archiveSession).mockResolvedValueOnce(true)
const store = useChatStore()
await store.loadSessions()
const ok = await store.archiveSession('archive-me')
expect(ok).toBe(true)
expect(archiveSession).toHaveBeenCalledWith('archive-me')
expect(store.sessions.map(session => session.id)).toEqual(['active-session'])
})
it('clears the active session when archiving the only runtime session', async () => {
vi.mocked(fetchSessions)
.mockResolvedValueOnce([
makeSession('archive-me', { started_at: 1000, last_active: 1000 }),
] as any)
.mockResolvedValueOnce([])
vi.mocked(archiveSession).mockResolvedValueOnce(true)
const store = useChatStore()
await store.loadSessions()
expect(store.activeSessionId).toBe('archive-me')
const ok = await store.archiveSession('archive-me')
expect(ok).toBe(true)
expect(archiveSession).toHaveBeenCalledWith('archive-me')
expect(store.sessions).toEqual([])
expect(store.activeSessionId).toBeNull()
expect(store.activeSession).toBeNull()
})
})