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' } }], }) }) }) })