1
import { afterEach, describe, expect, it, vi } from 'vitest';
3
import { MESSAGE_CANCEL_FLAT } from '@/const/message';
4
import { ChatMessageError } from '@/types/message';
5
import { sleep } from '@/utils/sleep';
7
import { FetchEventSourceInit } from '../fetchEventSource';
8
import { fetchEventSource } from '../fetchEventSource';
9
import { fetchSSE } from '../fetchSSE';
12
vi.mock('i18next', () => ({
13
t: vi.fn((key) => `translated_${key}`),
16
vi.mock('../fetchEventSource', () => ({
17
fetchEventSource: vi.fn(),
25
describe('fetchSSE', () => {
26
it('should handle text event correctly', async () => {
27
const mockOnMessageHandle = vi.fn();
28
const mockOnFinish = vi.fn();
30
(fetchEventSource as any).mockImplementationOnce(
31
(url: string, options: FetchEventSourceInit) => {
32
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
33
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
34
options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
39
onMessageHandle: mockOnMessageHandle,
40
onFinish: mockOnFinish,
44
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'text' });
45
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'text' });
46
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
54
it('should handle tool_calls event correctly', async () => {
55
const mockOnMessageHandle = vi.fn();
56
const mockOnFinish = vi.fn();
58
(fetchEventSource as any).mockImplementationOnce(
59
(url: string, options: FetchEventSourceInit) => {
60
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
63
data: JSON.stringify([
64
{ index: 0, id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
69
data: JSON.stringify([
70
{ index: 1, id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
77
onMessageHandle: mockOnMessageHandle,
78
onFinish: mockOnFinish,
82
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
83
tool_calls: [{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } }],
86
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
88
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
89
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
93
expect(mockOnFinish).toHaveBeenCalledWith('', {
96
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
97
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
104
it('should call onMessageHandle with full text if no message event', async () => {
105
const mockOnMessageHandle = vi.fn();
106
const mockOnFinish = vi.fn();
108
(fetchEventSource as any).mockImplementationOnce(
109
(url: string, options: FetchEventSourceInit) => {
110
const res = new Response('Hello World', { status: 200, statusText: 'OK' });
111
options.onopen!(res as any);
115
await fetchSSE('/', { onMessageHandle: mockOnMessageHandle, onFinish: mockOnFinish });
117
expect(mockOnMessageHandle).toHaveBeenCalledWith({ text: 'Hello World', type: 'text' });
118
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
120
toolCalls: undefined,
126
it('should handle text event with smoothing correctly', async () => {
127
const mockOnMessageHandle = vi.fn();
128
const mockOnFinish = vi.fn();
130
(fetchEventSource as any).mockImplementationOnce(
131
async (url: string, options: FetchEventSourceInit) => {
132
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
133
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
135
options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
139
await fetchSSE('/', {
140
onMessageHandle: mockOnMessageHandle,
141
onFinish: mockOnFinish,
145
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'text' });
146
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'text' });
147
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' World', type: 'text' });
148
// more assertions for each character...
149
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
151
toolCalls: undefined,
157
it('should handle tool_calls event with smoothing correctly', async () => {
158
const mockOnMessageHandle = vi.fn();
159
const mockOnFinish = vi.fn();
161
(fetchEventSource as any).mockImplementationOnce(
162
(url: string, options: FetchEventSourceInit) => {
163
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
166
data: JSON.stringify([
167
{ index: 0, id: '1', type: 'function', function: { name: 'func1', arguments: 'a' } },
172
data: JSON.stringify([
173
{ index: 0, function: { arguments: 'rg1' } },
174
{ index: 1, id: '2', type: 'function', function: { name: 'func2', arguments: 'a' } },
179
data: JSON.stringify([{ index: 1, function: { arguments: 'rg2' } }]),
184
await fetchSSE('/', {
185
onMessageHandle: mockOnMessageHandle,
186
onFinish: mockOnFinish,
190
// TODO: need to check whether the `aarg1` is correct
191
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
192
isAnimationActives: [true, true],
194
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'aarg1' } },
195
{ function: { arguments: 'aarg2', name: 'func2' }, id: '2', type: 'function' },
199
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
200
isAnimationActives: [true, true],
202
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'aarg1' } },
203
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'aarg2' } },
208
// more assertions for each character...
209
expect(mockOnFinish).toHaveBeenCalledWith('', {
212
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
213
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
220
it('should handle request interruption and resumption correctly', async () => {
221
const mockOnMessageHandle = vi.fn();
222
const mockOnFinish = vi.fn();
223
const abortController = new AbortController();
225
(fetchEventSource as any).mockImplementationOnce(
226
async (url: string, options: FetchEventSourceInit) => {
227
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
228
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
230
abortController.abort();
231
options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
235
await fetchSSE('/', {
236
onMessageHandle: mockOnMessageHandle,
237
onFinish: mockOnFinish,
238
signal: abortController.signal,
242
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'text' });
243
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'text' });
244
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' World', type: 'text' });
246
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
253
it('should call onFinish with correct parameters for different finish types', async () => {
254
const mockOnFinish = vi.fn();
256
(fetchEventSource as any).mockImplementationOnce(
257
(url: string, options: FetchEventSourceInit) => {
258
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
259
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
260
options.onerror!({ name: 'AbortError' });
264
await fetchSSE('/', { onFinish: mockOnFinish, smoothing: false });
266
expect(mockOnFinish).toHaveBeenCalledWith('Hello', {
268
toolCalls: undefined,
273
(fetchEventSource as any).mockImplementationOnce(
274
(url: string, options: FetchEventSourceInit) => {
275
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
276
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
277
options.onerror!(new Error('Unknown error'));
281
await fetchSSE('/', { onFinish: mockOnFinish, smoothing: false });
283
expect(mockOnFinish).toHaveBeenCalledWith('Hello', {
285
toolCalls: undefined,
291
describe('onAbort', () => {
292
it('should call onAbort when AbortError is thrown', async () => {
293
const mockOnAbort = vi.fn();
295
(fetchEventSource as any).mockImplementationOnce(
296
(url: string, options: FetchEventSourceInit) => {
297
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
298
options.onerror!({ name: 'AbortError' });
302
await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
304
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
307
it('should call onAbort when MESSAGE_CANCEL_FLAT is thrown', async () => {
308
const mockOnAbort = vi.fn();
310
(fetchEventSource as any).mockImplementationOnce(
311
(url: string, options: FetchEventSourceInit) => {
312
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
313
options.onerror!(MESSAGE_CANCEL_FLAT);
317
await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
319
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
323
describe('onErrorHandle', () => {
324
it('should call onErrorHandle when Chat Message error is thrown', async () => {
325
const mockOnErrorHandle = vi.fn();
326
const mockError: ChatMessageError = {
328
message: 'StreamChunkError',
329
type: 'StreamChunkError',
332
(fetchEventSource as any).mockImplementationOnce(
333
(url: string, options: FetchEventSourceInit) => {
334
options.onerror!(mockError);
339
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
342
expect(mockOnErrorHandle).toHaveBeenCalledWith(mockError);
345
it('should call onErrorHandle when Unknown error is thrown', async () => {
346
const mockOnErrorHandle = vi.fn();
347
const mockError = new Error('Unknown error');
349
(fetchEventSource as any).mockImplementationOnce(
350
(url: string, options: FetchEventSourceInit) => {
351
options.onerror!(mockError);
356
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
359
expect(mockOnErrorHandle).toHaveBeenCalledWith({
360
type: 'UnknownChatFetchError',
361
message: 'Unknown error',
363
message: 'Unknown error',
365
stack: expect.any(String),
370
it('should call onErrorHandle when response is not ok', async () => {
371
const mockOnErrorHandle = vi.fn();
373
(fetchEventSource as any).mockImplementationOnce(
374
async (url: string, options: FetchEventSourceInit) => {
375
const res = new Response(JSON.stringify({ errorType: 'SomeError' }), {
381
await options.onopen!(res as any);
387
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
389
expect(mockOnErrorHandle).toHaveBeenCalledWith({
391
message: 'translated_response.SomeError',
397
it('should call onErrorHandle when stream chunk has error type', async () => {
398
const mockOnErrorHandle = vi.fn();
400
type: 'StreamChunkError',
402
body: { message: 'abc', context: {} },
405
(fetchEventSource as any).mockImplementationOnce(
406
(url: string, options: FetchEventSourceInit) => {
409
data: JSON.stringify(mockError),
415
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
418
expect(mockOnErrorHandle).toHaveBeenCalledWith(mockError);
421
it('should call onErrorHandle when stream chunk is not valid json', async () => {
422
const mockOnErrorHandle = vi.fn();
423
const mockError = 'abc';
425
(fetchEventSource as any).mockImplementationOnce(
426
(url: string, options: FetchEventSourceInit) => {
427
options.onmessage!({ event: 'text', data: mockError } as any);
432
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
435
expect(mockOnErrorHandle).toHaveBeenCalledWith({
440
message: 'Unexpected token a in JSON at position 0',
445
'chat response streaming chunk parse error, please contact your API Provider to fix it.',
447
message: 'parse error',
448
type: 'StreamChunkError',