396 lines
10 KiB
TypeScript
396 lines
10 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import { useMessages } from '../useMessages'
|
|
import { ThreadMessage } from '@janhq/core'
|
|
|
|
// Mock the ServiceHub
|
|
const mockCreateMessage = vi.fn()
|
|
const mockDeleteMessage = vi.fn()
|
|
|
|
vi.mock('@/hooks/useServiceHub', () => ({
|
|
getServiceHub: () => ({
|
|
messages: () => ({
|
|
createMessage: mockCreateMessage,
|
|
deleteMessage: mockDeleteMessage,
|
|
}),
|
|
}),
|
|
}))
|
|
|
|
vi.mock('./useAssistant', () => ({
|
|
useAssistant: {
|
|
getState: vi.fn(() => ({
|
|
currentAssistant: {
|
|
id: 'test-assistant',
|
|
name: 'Test Assistant',
|
|
avatar: 'test-avatar.png',
|
|
instructions: 'Test instructions',
|
|
parameters: 'test parameters',
|
|
},
|
|
assistants: [{
|
|
id: 'test-assistant',
|
|
name: 'Test Assistant',
|
|
avatar: 'test-avatar.png',
|
|
instructions: 'Test instructions',
|
|
parameters: 'test parameters',
|
|
}],
|
|
})),
|
|
},
|
|
}))
|
|
|
|
describe('useMessages', () => {
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
// Reset store state
|
|
useMessages.setState({ messages: {} })
|
|
})
|
|
|
|
it('should initialize with empty messages', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
expect(result.current.messages).toEqual({})
|
|
})
|
|
|
|
describe('getMessages', () => {
|
|
it('should return empty array for non-existent thread', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const messages = result.current.getMessages('non-existent-thread')
|
|
expect(messages).toEqual([])
|
|
})
|
|
|
|
it('should return messages for existing thread', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const testMessages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Hello',
|
|
created_at: Date.now(),
|
|
},
|
|
{
|
|
id: 'msg2',
|
|
thread_id: 'thread1',
|
|
role: 'assistant',
|
|
content: 'Hi there!',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', testMessages)
|
|
})
|
|
|
|
const messages = result.current.getMessages('thread1')
|
|
expect(messages).toEqual(testMessages)
|
|
})
|
|
})
|
|
|
|
describe('setMessages', () => {
|
|
it('should set messages for a thread', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const testMessages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Hello',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', testMessages)
|
|
})
|
|
|
|
expect(result.current.messages['thread1']).toEqual(testMessages)
|
|
})
|
|
|
|
it('should handle multiple threads', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const thread1Messages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Hello from thread 1',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
const thread2Messages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg2',
|
|
thread_id: 'thread2',
|
|
role: 'user',
|
|
content: 'Hello from thread 2',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', thread1Messages)
|
|
result.current.setMessages('thread2', thread2Messages)
|
|
})
|
|
|
|
expect(result.current.messages['thread1']).toEqual(thread1Messages)
|
|
expect(result.current.messages['thread2']).toEqual(thread2Messages)
|
|
})
|
|
|
|
it('should replace existing messages', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const initialMessages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Initial message',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
const newMessages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg2',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'New message',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', initialMessages)
|
|
})
|
|
|
|
expect(result.current.messages['thread1']).toEqual(initialMessages)
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', newMessages)
|
|
})
|
|
|
|
expect(result.current.messages['thread1']).toEqual(newMessages)
|
|
})
|
|
})
|
|
|
|
describe('addMessage', () => {
|
|
it('should add message and call createMessage service', async () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const mockCreatedMessage: ThreadMessage = {
|
|
id: 'created-msg',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
created_at: Date.now(),
|
|
metadata: {
|
|
assistant: {
|
|
id: 'test-assistant',
|
|
name: 'Test Assistant',
|
|
avatar: 'test-avatar.png',
|
|
instructions: 'Test instructions',
|
|
parameters: 'test parameters',
|
|
},
|
|
},
|
|
}
|
|
|
|
mockCreateMessage.mockResolvedValue(mockCreatedMessage)
|
|
|
|
const messageToAdd: ThreadMessage = {
|
|
id: 'temp-msg',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
created_at: Date.now(),
|
|
}
|
|
|
|
act(() => {
|
|
result.current.addMessage(messageToAdd)
|
|
})
|
|
|
|
expect(mockCreateMessage).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
...messageToAdd,
|
|
metadata: expect.objectContaining({
|
|
assistant: expect.objectContaining({
|
|
id: expect.any(String),
|
|
name: expect.any(String),
|
|
}),
|
|
}),
|
|
})
|
|
)
|
|
|
|
// Wait for async operation
|
|
await vi.waitFor(() => {
|
|
expect(result.current.messages['thread1']).toContainEqual(mockCreatedMessage)
|
|
})
|
|
})
|
|
|
|
it('should handle message without created_at', async () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const mockCreatedMessage: ThreadMessage = {
|
|
id: 'created-msg',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
created_at: Date.now(),
|
|
}
|
|
|
|
mockCreateMessage.mockResolvedValue(mockCreatedMessage)
|
|
|
|
const messageToAdd: ThreadMessage = {
|
|
id: 'temp-msg',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
// no created_at provided
|
|
} as ThreadMessage
|
|
|
|
act(() => {
|
|
result.current.addMessage(messageToAdd)
|
|
})
|
|
|
|
expect(mockCreateMessage).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
created_at: expect.any(Number),
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should preserve existing metadata', async () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const mockCreatedMessage: ThreadMessage = {
|
|
id: 'created-msg',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
created_at: Date.now(),
|
|
}
|
|
|
|
mockCreateMessage.mockResolvedValue(mockCreatedMessage)
|
|
|
|
const messageToAdd: ThreadMessage = {
|
|
id: 'temp-msg',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
created_at: Date.now(),
|
|
metadata: {
|
|
customField: 'custom value',
|
|
},
|
|
}
|
|
|
|
act(() => {
|
|
result.current.addMessage(messageToAdd)
|
|
})
|
|
|
|
expect(mockCreateMessage).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
customField: 'custom value',
|
|
assistant: expect.any(Object),
|
|
}),
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('deleteMessage', () => {
|
|
it('should delete message and call deleteMessage service', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const testMessages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Message 1',
|
|
created_at: Date.now(),
|
|
},
|
|
{
|
|
id: 'msg2',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Message 2',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', testMessages)
|
|
})
|
|
|
|
act(() => {
|
|
result.current.deleteMessage('thread1', 'msg1')
|
|
})
|
|
|
|
expect(mockDeleteMessage).toHaveBeenCalledWith('thread1', 'msg1')
|
|
expect(result.current.messages['thread1']).toEqual([testMessages[1]])
|
|
})
|
|
|
|
it('should handle deleting from empty thread', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
act(() => {
|
|
result.current.deleteMessage('empty-thread', 'non-existent-msg')
|
|
})
|
|
|
|
expect(mockDeleteMessage).toHaveBeenCalledWith('empty-thread', 'non-existent-msg')
|
|
expect(result.current.messages['empty-thread']).toEqual([])
|
|
})
|
|
|
|
it('should handle deleting non-existent message', () => {
|
|
const { result } = renderHook(() => useMessages())
|
|
|
|
const testMessages: ThreadMessage[] = [
|
|
{
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Message 1',
|
|
created_at: Date.now(),
|
|
},
|
|
]
|
|
|
|
act(() => {
|
|
result.current.setMessages('thread1', testMessages)
|
|
})
|
|
|
|
act(() => {
|
|
result.current.deleteMessage('thread1', 'non-existent-msg')
|
|
})
|
|
|
|
expect(mockDeleteMessage).toHaveBeenCalledWith('thread1', 'non-existent-msg')
|
|
expect(result.current.messages['thread1']).toEqual(testMessages)
|
|
})
|
|
})
|
|
|
|
describe('state management', () => {
|
|
it('should maintain state across multiple hook instances', () => {
|
|
const { result: result1 } = renderHook(() => useMessages())
|
|
const { result: result2 } = renderHook(() => useMessages())
|
|
|
|
const testMessage: ThreadMessage = {
|
|
id: 'msg1',
|
|
thread_id: 'thread1',
|
|
role: 'user',
|
|
content: 'Test message',
|
|
created_at: Date.now(),
|
|
}
|
|
|
|
act(() => {
|
|
result1.current.setMessages('thread1', [testMessage])
|
|
})
|
|
|
|
expect(result2.current.getMessages('thread1')).toEqual([testMessage])
|
|
})
|
|
})
|
|
})
|