jan/web-app/src/services/__tests__/threads.test.ts
2025-07-15 22:29:28 +07:00

431 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchThreads, createThread, updateThread, deleteThread } from '../threads'
import { ExtensionManager } from '@/lib/extension'
import { ConversationalExtension, ExtensionTypeEnum } from '@janhq/core'
import { defaultAssistant } from '@/hooks/useAssistant'
// Mock ExtensionManager
vi.mock('@/lib/extension', () => ({
ExtensionManager: {
getInstance: vi.fn(),
},
}))
vi.mock('@/hooks/useAssistant', () => ({
defaultAssistant: {
id: 'jan',
name: 'Jan',
instructions: 'You are a helpful assistant.',
},
}))
describe('threads service', () => {
const mockConversationalExtension = {
listThreads: vi.fn(),
createThread: vi.fn(),
modifyThread: vi.fn(),
deleteThread: vi.fn(),
}
const mockExtensionManager = {
get: vi.fn().mockReturnValue(mockConversationalExtension),
}
beforeEach(() => {
vi.clearAllMocks()
;(ExtensionManager.getInstance as any).mockReturnValue(mockExtensionManager)
})
describe('fetchThreads', () => {
it('should fetch and transform threads successfully', async () => {
const mockThreads = [
{
id: '1',
title: 'Test Thread',
updated: 1234567890,
metadata: { order: 1, is_favorite: true },
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
},
]
mockConversationalExtension.listThreads.mockResolvedValue(mockThreads)
const result = await fetchThreads()
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
id: '1',
title: 'Test Thread',
updated: 1234567890,
order: 1,
isFavorite: true,
model: { id: 'gpt-4', provider: 'openai' },
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
})
})
it('should handle empty threads array', async () => {
mockConversationalExtension.listThreads.mockResolvedValue([])
const result = await fetchThreads()
expect(result).toEqual([])
})
it('should handle error and return empty array', async () => {
mockConversationalExtension.listThreads.mockRejectedValue(new Error('API Error'))
const result = await fetchThreads()
expect(result).toEqual([])
})
it('should handle null/undefined response', async () => {
mockConversationalExtension.listThreads.mockResolvedValue(null)
const result = await fetchThreads()
expect(result).toEqual([])
})
})
describe('createThread', () => {
it('should create thread successfully', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
model: { id: 'gpt-4', provider: 'openai' },
assistants: [defaultAssistant],
order: 1,
}
const mockCreatedThread = {
id: '1',
title: 'New Thread',
updated: 1234567890,
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
metadata: { order: 1 },
}
mockConversationalExtension.createThread.mockResolvedValue(mockCreatedThread)
const result = await createThread(inputThread as Thread)
expect(result).toMatchObject({
id: '1',
title: 'New Thread',
updated: 1234567890,
model: { id: 'gpt-4', provider: 'openai' },
order: 1,
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
})
})
it('should handle creation error and return original thread', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
model: { id: 'gpt-4', provider: 'openai' },
}
mockConversationalExtension.createThread.mockRejectedValue(new Error('Creation failed'))
const result = await createThread(inputThread as Thread)
expect(result).toEqual(inputThread)
})
})
describe('updateThread', () => {
it('should update thread successfully', async () => {
const thread = {
id: '1',
title: 'Updated Thread',
model: { id: 'gpt-4', provider: 'openai' },
assistants: [defaultAssistant],
isFavorite: true,
order: 2,
}
const result = updateThread(thread as Thread)
expect(mockConversationalExtension.modifyThread).toHaveBeenCalledWith(
expect.objectContaining({
id: '1',
title: 'Updated Thread',
assistants: expect.arrayContaining([
expect.objectContaining({
model: { id: 'gpt-4', engine: 'openai' },
}),
]),
metadata: { is_favorite: true, order: 2 },
})
)
})
})
describe('deleteThread', () => {
it('should delete thread successfully', () => {
const threadId = '1'
deleteThread(threadId)
expect(mockConversationalExtension.deleteThread).toHaveBeenCalledWith(threadId)
})
})
describe('edge cases and error handling', () => {
it('should handle fetchThreads when extension manager returns null', async () => {
;(ExtensionManager.getInstance as any).mockReturnValue({
get: vi.fn().mockReturnValue(null)
})
const result = await fetchThreads()
expect(result).toEqual([])
})
it('should handle createThread when extension manager returns null', async () => {
;(ExtensionManager.getInstance as any).mockReturnValue({
get: vi.fn().mockReturnValue(null)
})
const inputThread = {
id: '1',
title: 'Test Thread',
model: { id: 'gpt-4', provider: 'openai' },
}
const result = await createThread(inputThread as Thread)
expect(result).toEqual(inputThread)
})
it('should handle updateThread when extension manager returns null', () => {
;(ExtensionManager.getInstance as any).mockReturnValue({
get: vi.fn().mockReturnValue(null)
})
const thread = {
id: '1',
title: 'Test Thread',
model: { id: 'gpt-4', provider: 'openai' },
}
const result = updateThread(thread as Thread)
expect(result).toBeUndefined()
})
it('should handle deleteThread when extension manager returns null', () => {
;(ExtensionManager.getInstance as any).mockReturnValue({
get: vi.fn().mockReturnValue(null)
})
const result = deleteThread('test-id')
expect(result).toBeUndefined()
})
it('should handle fetchThreads with threads missing metadata', async () => {
const mockThreads = [
{
id: '1',
title: 'Test Thread',
// missing metadata
},
]
mockConversationalExtension.listThreads.mockResolvedValue(mockThreads)
const result = await fetchThreads()
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
id: '1',
title: 'Test Thread',
updated: 0,
order: undefined,
isFavorite: undefined,
assistants: [defaultAssistant],
})
})
it('should handle fetchThreads with threads missing assistants', async () => {
const mockThreads = [
{
id: '1',
title: 'Test Thread',
updated: 1234567890,
metadata: { order: 1, is_favorite: true },
// missing assistants
},
]
mockConversationalExtension.listThreads.mockResolvedValue(mockThreads)
const result = await fetchThreads()
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
id: '1',
title: 'Test Thread',
updated: 1234567890,
order: 1,
isFavorite: true,
assistants: [defaultAssistant],
})
})
it('should handle createThread with missing model info', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
// missing model
assistants: [defaultAssistant],
order: 1,
}
const mockCreatedThread = {
id: '1',
title: 'New Thread',
updated: 1234567890,
assistants: [{ model: { id: '*', engine: 'llamacpp' } }],
metadata: { order: 1 },
}
mockConversationalExtension.createThread.mockResolvedValue(mockCreatedThread)
const result = await createThread(inputThread as Thread)
expect(mockConversationalExtension.createThread).toHaveBeenCalledWith(
expect.objectContaining({
assistants: [
expect.objectContaining({
model: { id: '*', engine: 'llamacpp' },
}),
],
})
)
})
it('should handle createThread with missing assistants', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
model: { id: 'gpt-4', provider: 'openai' },
// missing assistants
order: 1,
}
const mockCreatedThread = {
id: '1',
title: 'New Thread',
updated: 1234567890,
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
metadata: { order: 1 },
}
mockConversationalExtension.createThread.mockResolvedValue(mockCreatedThread)
const result = await createThread(inputThread as Thread)
expect(mockConversationalExtension.createThread).toHaveBeenCalledWith(
expect.objectContaining({
assistants: [
expect.objectContaining({
...defaultAssistant,
model: { id: 'gpt-4', engine: 'openai' },
}),
],
})
)
})
it('should handle updateThread with missing assistants', () => {
const thread = {
id: '1',
title: 'Updated Thread',
model: { id: 'gpt-4', provider: 'openai' },
// missing assistants
isFavorite: true,
order: 2,
}
updateThread(thread as Thread)
expect(mockConversationalExtension.modifyThread).toHaveBeenCalledWith(
expect.objectContaining({
assistants: [
{
model: { id: 'gpt-4', engine: 'openai' },
id: 'jan',
name: 'Jan',
},
],
})
)
})
it('should handle updateThread with missing model info', () => {
const thread = {
id: '1',
title: 'Updated Thread',
// missing model
assistants: [defaultAssistant],
isFavorite: true,
order: 2,
}
updateThread(thread as Thread)
expect(mockConversationalExtension.modifyThread).toHaveBeenCalledWith(
expect.objectContaining({
assistants: [
expect.objectContaining({
model: { id: '*', engine: 'llamacpp' },
}),
],
})
)
})
it('should handle fetchThreads with non-array response', async () => {
mockConversationalExtension.listThreads.mockResolvedValue('not-an-array')
const result = await fetchThreads()
expect(result).toEqual([])
})
it('should handle createThread with missing metadata in response', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
model: { id: 'gpt-4', provider: 'openai' },
order: 1,
}
const mockCreatedThread = {
id: '1',
title: 'New Thread',
updated: 1234567890,
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
// missing metadata
}
mockConversationalExtension.createThread.mockResolvedValue(mockCreatedThread)
const result = await createThread(inputThread as Thread)
expect(result).toMatchObject({
id: '1',
title: 'New Thread',
updated: 1234567890,
model: { id: 'gpt-4', provider: 'openai' },
order: 1, // Should fall back to original thread order
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
})
})
})
})