Files
hermes-ui/tests/client/chat-store-session-order.test.ts
T
toor 3ca960f388
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
优化页面交互并增强 Provider 配置能力
- 表单下拉框不再用 label 包裹,改为 div + aria-label,避免点击后
  下拉面板瞬间关闭
- Provider 卡片新增「从上游获取模型」和「修改」入口,支持编辑
  base URL、API Key 与 api_mode,凭据写入对应的环境变量
- 服务端抽出 provider-model-discovery 统一不同 api_mode 的模型发现,
  旧的 custom_providers 与新的 providers 字典结构都能正确更新
- Agent Bridge 改为后台启动,HTTP 接口在 Python 运行时就绪前即可用;
  worker 连接被拒时按类型重试,不重放可能已产生副作用的请求
- 断线重连后恢复 profiles、models 与会话列表,健康检查改为自调度
  的 setTimeout,离线时 2 秒后重试
- 批量删除确认文案改用 Popconfirm content,避免变成侧栏触发器文本
2026-10-22 14:29:48 +08:00

157 lines
5.2 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()),
onSessionWorkspaceUpdated: 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()
})
it('keeps a failed initial session load retryable and restores sessions on retry', async () => {
vi.mocked(fetchSessions).mockRejectedValue(new Error('backend starting'))
const store = useChatStore()
expect(await store.loadSessions()).toBe(false)
expect(store.sessionsLoaded).toBe(false)
vi.mocked(fetchSessions).mockResolvedValue([makeSession('restored', { started_at: 1000 })] as any)
expect(await store.loadSessions()).toBe(true)
expect(store.sessionsLoaded).toBe(true)
expect(store.activeSessionId).toBe('restored')
vi.mocked(fetchSessions).mockRejectedValue(new Error('restart'))
expect(await store.loadSessions()).toBe(false)
expect(store.sessions.map(session => session.id)).toEqual(['restored'])
})
})