Skip to content

Commit 2ff15dc

Browse files
committed
chore: update vitest configuration and enhance vectorUtils tests
- Added ignored files in vitest configuration for better test management. - Expanded tests in vectorUtils to cover scenarios for handling document unique identifiers during update failures.
1 parent 6e0d0fb commit 2ff15dc

File tree

10 files changed

+851
-0
lines changed

10 files changed

+851
-0
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/* @vitest-environment node */
2+
3+
import { describe, expect, it, vi } from 'vitest'
4+
import { createMockReq, createMockRes } from '~/test-utils/nextApi'
5+
6+
const hoisted = vi.hoisted(() => ({
7+
vectorSearchWithDrizzle: vi.fn(),
8+
}))
9+
10+
vi.mock('~/pages/api/authorization', () => ({
11+
withCourseAccessFromRequest:
12+
() => (h: (req: any, res: any) => Promise<void>) =>
13+
h,
14+
}))
15+
16+
vi.mock('~/db/vectorSearch', () => ({
17+
vectorSearchWithDrizzle: hoisted.vectorSearchWithDrizzle,
18+
}))
19+
20+
import handler from '~/pages/api/getContextsDrizzle'
21+
22+
describe('getContextsDrizzle API', () => {
23+
it('returns 405 for non-POST methods', async () => {
24+
const res = createMockRes()
25+
await handler(createMockReq({ method: 'GET' }) as any, res as any)
26+
expect(res.status).toHaveBeenCalledWith(405)
27+
expect(res.json).toHaveBeenCalledWith({ error: 'Method not allowed' })
28+
})
29+
30+
it('returns 400 when queryEmbedding is not an array', async () => {
31+
const res = createMockRes()
32+
await handler(
33+
createMockReq({
34+
method: 'POST',
35+
body: { course_name: 'CS101', queryEmbedding: 'not-array' },
36+
}) as any,
37+
res as any,
38+
)
39+
expect(res.status).toHaveBeenCalledWith(400)
40+
expect(hoisted.vectorSearchWithDrizzle).not.toHaveBeenCalled()
41+
})
42+
43+
it('returns 400 when queryEmbedding is missing', async () => {
44+
const res = createMockRes()
45+
await handler(
46+
createMockReq({
47+
method: 'POST',
48+
body: { course_name: 'CS101' },
49+
}) as any,
50+
res as any,
51+
)
52+
expect(res.status).toHaveBeenCalledWith(400)
53+
expect(res.json).toHaveBeenCalledWith({
54+
error:
55+
'queryEmbedding (number[]) and course_name are required. Get queryEmbedding from your embedding API or backend.',
56+
})
57+
expect(hoisted.vectorSearchWithDrizzle).not.toHaveBeenCalled()
58+
})
59+
60+
it('returns 400 when queryEmbedding is empty array', async () => {
61+
const res = createMockRes()
62+
await handler(
63+
createMockReq({
64+
method: 'POST',
65+
body: { course_name: 'CS101', queryEmbedding: [] },
66+
}) as any,
67+
res as any,
68+
)
69+
expect(res.status).toHaveBeenCalledWith(400)
70+
expect(hoisted.vectorSearchWithDrizzle).not.toHaveBeenCalled()
71+
})
72+
73+
it('returns 400 when course_name is missing', async () => {
74+
const res = createMockRes()
75+
await handler(
76+
createMockReq({
77+
method: 'POST',
78+
body: { queryEmbedding: [0.1, 0.2] },
79+
}) as any,
80+
res as any,
81+
)
82+
expect(res.status).toHaveBeenCalledWith(400)
83+
expect(hoisted.vectorSearchWithDrizzle).not.toHaveBeenCalled()
84+
})
85+
86+
it('returns 200 with data from vectorSearchWithDrizzle', async () => {
87+
const data = [
88+
{
89+
id: '1',
90+
text: 'context one',
91+
metadata: { course_name: 'CS101' },
92+
},
93+
]
94+
hoisted.vectorSearchWithDrizzle.mockResolvedValueOnce(data)
95+
96+
const res = createMockRes()
97+
await handler(
98+
createMockReq({
99+
method: 'POST',
100+
body: {
101+
queryEmbedding: [0.1, 0.2, 0.3],
102+
course_name: 'CS225',
103+
doc_groups: ['g1'],
104+
disabled_doc_groups: ['g2'],
105+
public_doc_groups: [],
106+
conversation_id: 'conv-1',
107+
top_n: 50,
108+
},
109+
}) as any,
110+
res as any,
111+
)
112+
113+
expect(hoisted.vectorSearchWithDrizzle).toHaveBeenCalledWith({
114+
queryEmbedding: [0.1, 0.2, 0.3],
115+
course_name: 'CS225',
116+
doc_groups: ['g1'],
117+
disabled_doc_groups: ['g2'],
118+
public_doc_groups: [],
119+
conversation_id: 'conv-1',
120+
top_n: 50,
121+
})
122+
expect(res.status).toHaveBeenCalledWith(200)
123+
expect(res.json).toHaveBeenCalledWith(data)
124+
})
125+
126+
it('passes default doc_groups, disabled_doc_groups, public_doc_groups and top_n when omitted', async () => {
127+
hoisted.vectorSearchWithDrizzle.mockResolvedValueOnce([])
128+
129+
const res = createMockRes()
130+
await handler(
131+
createMockReq({
132+
method: 'POST',
133+
body: {
134+
queryEmbedding: [0.1],
135+
course_name: 'CS101',
136+
},
137+
}) as any,
138+
res as any,
139+
)
140+
141+
expect(hoisted.vectorSearchWithDrizzle).toHaveBeenCalledWith({
142+
queryEmbedding: [0.1],
143+
course_name: 'CS101',
144+
doc_groups: [],
145+
disabled_doc_groups: [],
146+
public_doc_groups: [],
147+
conversation_id: undefined,
148+
top_n: 100,
149+
})
150+
expect(res.status).toHaveBeenCalledWith(200)
151+
})
152+
153+
it('returns 500 when vectorSearchWithDrizzle throws', async () => {
154+
vi.spyOn(console, 'error').mockImplementation(() => {})
155+
hoisted.vectorSearchWithDrizzle.mockRejectedValueOnce(new Error('db error'))
156+
157+
const res = createMockRes()
158+
await handler(
159+
createMockReq({
160+
method: 'POST',
161+
body: {
162+
queryEmbedding: [0.1],
163+
course_name: 'CS101',
164+
},
165+
}) as any,
166+
res as any,
167+
)
168+
169+
expect(res.status).toHaveBeenCalledWith(500)
170+
expect(res.json).toHaveBeenCalledWith({
171+
error: 'Internal server error while fetching contexts via Drizzle',
172+
data: [],
173+
})
174+
})
175+
})
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { screen } from '@testing-library/react'
4+
import userEvent from '@testing-library/user-event'
5+
6+
import { renderWithProviders } from '~/test-utils/renderWithProviders'
7+
import { DocumentGroupsItem } from '../DocumentGroupsItem'
8+
9+
vi.mock('@mantine/hooks', () => ({
10+
useMediaQuery: () => false,
11+
}))
12+
13+
describe('DocumentGroupsItem', () => {
14+
it('renders document groups and filters by search', async () => {
15+
const user = userEvent.setup()
16+
const dispatch = vi.fn()
17+
const documentGroups = [
18+
{ id: 'g1', name: 'Lectures', checked: true },
19+
{ id: 'g2', name: 'Assignments', checked: false },
20+
{ id: 'g3', name: 'Readings', checked: true },
21+
] as any[]
22+
23+
renderWithProviders(<DocumentGroupsItem />, {
24+
homeState: { documentGroups },
25+
homeContext: { dispatch } as any,
26+
})
27+
28+
expect(screen.getByText('Document Groups')).toBeInTheDocument()
29+
expect(screen.getByText('Lectures')).toBeInTheDocument()
30+
expect(screen.getByText('Assignments')).toBeInTheDocument()
31+
expect(screen.getByText('Readings')).toBeInTheDocument()
32+
33+
const searchInput = screen.getByPlaceholderText('Search by Document Group')
34+
await user.type(searchInput, 'lect')
35+
expect(screen.getByText('Lectures')).toBeInTheDocument()
36+
expect(screen.queryByText('Assignments')).not.toBeInTheDocument()
37+
expect(screen.queryByText('Readings')).not.toBeInTheDocument()
38+
})
39+
40+
it('dispatches when toggle is clicked', async () => {
41+
const user = userEvent.setup()
42+
const dispatch = vi.fn()
43+
const documentGroups = [
44+
{ id: 'g1', name: 'Lectures', checked: true },
45+
] as any[]
46+
47+
renderWithProviders(<DocumentGroupsItem />, {
48+
homeState: { documentGroups },
49+
homeContext: { dispatch } as any,
50+
})
51+
52+
const checkboxes = screen.queryAllByRole('checkbox')
53+
const switches = screen.queryAllByRole('switch')
54+
const toggle = checkboxes[0] ?? switches[0]
55+
expect(toggle).toBeTruthy()
56+
await user.click(toggle!)
57+
expect(dispatch).toHaveBeenCalledWith({
58+
field: 'documentGroups',
59+
value: [{ id: 'g1', name: 'Lectures', checked: false }],
60+
})
61+
})
62+
63+
it('shows no document groups found when filtered list is empty', async () => {
64+
const user = userEvent.setup()
65+
const documentGroups = [
66+
{ id: 'g1', name: 'Only One', checked: true },
67+
] as any[]
68+
69+
renderWithProviders(<DocumentGroupsItem />, {
70+
homeState: { documentGroups },
71+
homeContext: { dispatch: vi.fn() } as any,
72+
})
73+
74+
const searchInput = screen.getByPlaceholderText('Search by Document Group')
75+
await user.type(searchInput, 'xyz')
76+
expect(screen.getByText('No document groups found')).toBeInTheDocument()
77+
})
78+
})
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React from 'react'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { screen } from '@testing-library/react'
4+
5+
import { renderWithProviders } from '~/test-utils/renderWithProviders'
6+
import { makeMessage } from '~/test-utils/mocks/chat'
7+
import { MemoizedChatMessage } from '../MemoizedChatMessage'
8+
9+
vi.mock('../ChatMessage', () => ({
10+
ChatMessage: (props: {
11+
message: { content?: string; id?: string; feedback?: unknown }
12+
}) =>
13+
React.createElement(
14+
'div',
15+
{ 'data-testid': 'chat-message' },
16+
props.message?.content ?? '',
17+
),
18+
}))
19+
20+
describe('MemoizedChatMessage', () => {
21+
it('renders message content via ChatMessage', () => {
22+
const message = makeMessage({
23+
id: 'm1',
24+
role: 'user',
25+
content: 'Hello',
26+
feedback: null,
27+
}) as any
28+
29+
renderWithProviders(
30+
<MemoizedChatMessage
31+
message={message}
32+
messageIndex={0}
33+
courseName="CS101"
34+
/>,
35+
)
36+
expect(screen.getByTestId('chat-message')).toHaveTextContent('Hello')
37+
})
38+
39+
it('memo comparator: re-render with same content/id/feedback does not change displayed content', () => {
40+
const message = makeMessage({
41+
id: 'm1',
42+
role: 'assistant',
43+
content: 'Same',
44+
feedback: { rating: 1 },
45+
}) as any
46+
47+
const { rerender } = renderWithProviders(
48+
<MemoizedChatMessage
49+
message={message}
50+
messageIndex={0}
51+
courseName="CS101"
52+
/>,
53+
)
54+
expect(screen.getByTestId('chat-message')).toHaveTextContent('Same')
55+
56+
rerender(
57+
<MemoizedChatMessage
58+
message={{
59+
...message,
60+
content: 'Same',
61+
id: 'm1',
62+
feedback: { rating: 1 },
63+
}}
64+
messageIndex={0}
65+
courseName="CS101"
66+
/>,
67+
)
68+
expect(screen.getByTestId('chat-message')).toHaveTextContent('Same')
69+
})
70+
71+
it('memo comparator: re-render with different content triggers update', () => {
72+
const message1 = makeMessage({
73+
id: 'm1',
74+
role: 'user',
75+
content: 'First',
76+
feedback: null,
77+
}) as any
78+
79+
const { rerender } = renderWithProviders(
80+
<MemoizedChatMessage
81+
message={message1}
82+
messageIndex={0}
83+
courseName="CS101"
84+
/>,
85+
)
86+
expect(screen.getByTestId('chat-message')).toHaveTextContent('First')
87+
88+
const message2 = {
89+
...message1,
90+
content: 'Second',
91+
id: 'm1',
92+
feedback: null,
93+
}
94+
rerender(
95+
<MemoizedChatMessage
96+
message={message2}
97+
messageIndex={0}
98+
courseName="CS101"
99+
/>,
100+
)
101+
expect(screen.getByTestId('chat-message')).toHaveTextContent('Second')
102+
})
103+
104+
it('memo comparator: re-render with different feedback triggers update', () => {
105+
const message1 = makeMessage({
106+
id: 'm1',
107+
role: 'user',
108+
content: 'Same',
109+
feedback: { rating: 1 },
110+
}) as any
111+
112+
const { rerender } = renderWithProviders(
113+
<MemoizedChatMessage
114+
message={message1}
115+
messageIndex={0}
116+
courseName="CS101"
117+
/>,
118+
)
119+
120+
const message2 = { ...message1, feedback: { rating: -1 } }
121+
rerender(
122+
<MemoizedChatMessage
123+
message={message2}
124+
messageIndex={0}
125+
courseName="CS101"
126+
/>,
127+
)
128+
expect(screen.getByTestId('chat-message')).toHaveTextContent('Same')
129+
})
130+
})

0 commit comments

Comments
 (0)