1
1
import XRequest from '../index' ;
2
2
import xFetch from '../x-fetch' ;
3
3
4
+ import type { SSEOutput } from '../../x-stream' ;
5
+ import type { XRequestCallbacks , XRequestOptions } from '../index' ;
6
+
4
7
jest . mock ( '../x-fetch' , ( ) => jest . fn ( ) ) ;
5
8
6
- const sseChunks = [ 'event:message\ndata:{"id":"0","content":"He"}\n\n' ] ;
9
+ const SSE_SEPARATOR = '\n\n' ;
10
+
11
+ const ND_JSON_SEPARATOR = '\n' ;
12
+
13
+ const sseEvent : SSEOutput = { event : 'message' , data : '{"id":"0","content":"He"}' } ;
14
+
15
+ const sseData = `${ Object . keys ( sseEvent )
16
+ . map ( ( key ) => `${ key } :${ sseEvent [ key as keyof SSEOutput ] } ` )
17
+ . join ( ND_JSON_SEPARATOR ) } ${ SSE_SEPARATOR } `;
18
+
19
+ const ndJsonData = `${ JSON . stringify ( sseEvent ) } ${ ND_JSON_SEPARATOR } ${ JSON . stringify ( { ...sseEvent , event : 'delta' } ) } ` ;
20
+
21
+ const options : XRequestOptions = {
22
+ baseURL : 'https://api.example.com/v1/chat' ,
23
+ model : 'gpt-3.5-turbo' ,
24
+ dangerouslyApiKey : 'dangerouslyApiKey' ,
25
+ } ;
7
26
8
- function mockReadableStream ( ) {
27
+ const params = { messages : [ { role : 'user' , content : 'Hello' } ] } ;
28
+
29
+ function mockSSEReadableStream ( ) {
9
30
return new ReadableStream ( {
10
31
async start ( controller ) {
11
- for ( const chunk of sseChunks ) {
32
+ for ( const chunk of sseData . split ( SSE_SEPARATOR ) ) {
33
+ controller . enqueue ( new TextEncoder ( ) . encode ( chunk ) ) ;
34
+ }
35
+ controller . close ( ) ;
36
+ } ,
37
+ } ) ;
38
+ }
39
+
40
+ function mockNdJsonReadableStream ( ) {
41
+ return new ReadableStream ( {
42
+ async start ( controller ) {
43
+ for ( const chunk of ndJsonData . split ( ND_JSON_SEPARATOR ) ) {
12
44
controller . enqueue ( new TextEncoder ( ) . encode ( chunk ) ) ;
13
45
}
14
46
controller . close ( ) ;
@@ -17,109 +49,122 @@ function mockReadableStream() {
17
49
}
18
50
19
51
describe ( 'XRequest Class' , ( ) => {
20
- const baseURL = 'https://api.example.com/v1/chat' ;
21
- const model = 'gpt-3.5-turbo' ;
52
+ const callbacks : XRequestCallbacks < any > = {
53
+ onSuccess : jest . fn ( ) ,
54
+ onError : jest . fn ( ) ,
55
+ onUpdate : jest . fn ( ) ,
56
+ } ;
57
+
58
+ const mockedXFetch = xFetch as jest . Mock ;
59
+
60
+ let request : ReturnType < typeof XRequest > ;
22
61
23
62
beforeEach ( ( ) => {
24
63
jest . clearAllMocks ( ) ;
64
+ request = XRequest ( options ) ;
25
65
} ) ;
26
66
27
67
test ( 'should initialize with valid options' , ( ) => {
28
- const request = XRequest ( { baseURL, model } ) ;
29
-
30
- expect ( request . baseURL ) . toBe ( baseURL ) ;
31
- expect ( request . model ) . toBe ( model ) ;
68
+ expect ( request . baseURL ) . toBe ( options . baseURL ) ;
69
+ expect ( request . model ) . toBe ( options . model ) ;
32
70
} ) ;
33
71
34
72
test ( 'should throw error on invalid baseURL' , ( ) => {
35
- expect ( ( ) => XRequest ( { baseURL : '' , model } ) ) . toThrow ( 'The baseURL is not valid!' ) ;
73
+ expect ( ( ) => XRequest ( { baseURL : '' } ) ) . toThrow ( 'The baseURL is not valid!' ) ;
36
74
} ) ;
37
75
38
76
test ( 'should create request and handle successful JSON response' , async ( ) => {
39
- const onSuccess = jest . fn ( ) ;
40
- const onError = jest . fn ( ) ;
41
- const onUpdate = jest . fn ( ) ;
42
-
43
- const params = { messages : [ { role : 'user' , content : 'Hello' } ] } ;
44
-
45
- const mockedXFetch = xFetch as jest . Mock ;
46
-
47
77
mockedXFetch . mockResolvedValueOnce ( {
48
78
ok : true ,
49
79
status : 200 ,
50
80
headers : {
51
- get : jest . fn ( ) . mockReturnValue ( 'application/json' ) ,
81
+ get : jest . fn ( ) . mockReturnValue ( 'application/json; charset=utf-8 ' ) ,
52
82
} ,
53
- json : jest . fn ( ) . mockResolvedValueOnce ( { response : 'Hi there!' } ) ,
83
+ json : jest . fn ( ) . mockResolvedValueOnce ( params ) ,
54
84
} ) ;
55
-
56
- const request = XRequest ( { baseURL, model } ) ;
57
- await request . create ( params , { onSuccess, onError, onUpdate } ) ;
58
-
59
- expect ( onSuccess ) . toHaveBeenCalledWith ( [ { response : 'Hi there!' } ] ) ;
60
- expect ( onError ) . not . toHaveBeenCalled ( ) ;
61
- expect ( onUpdate ) . toHaveBeenCalled ( ) ;
85
+ await request . create ( params , callbacks ) ;
86
+ expect ( callbacks . onSuccess ) . toHaveBeenCalledWith ( [ params ] ) ;
87
+ expect ( callbacks . onError ) . not . toHaveBeenCalled ( ) ;
88
+ expect ( callbacks . onUpdate ) . toHaveBeenCalledWith ( params ) ;
62
89
} ) ;
63
90
64
91
test ( 'should create request and handle streaming response' , async ( ) => {
65
- const onSuccess = jest . fn ( ) ;
66
- const onError = jest . fn ( ) ;
67
- const onUpdate = jest . fn ( ) ;
68
- const params = { messages : [ { role : 'user' , content : 'Hello' } ] } ;
69
-
70
- const mockedXFetch = xFetch as jest . Mock ;
71
92
mockedXFetch . mockResolvedValueOnce ( {
72
93
headers : {
73
94
get : jest . fn ( ) . mockReturnValue ( 'text/event-stream' ) ,
74
95
} ,
75
- body : mockReadableStream ( ) ,
96
+ body : mockSSEReadableStream ( ) ,
76
97
} ) ;
98
+ await request . create ( params , callbacks ) ;
99
+ expect ( callbacks . onSuccess ) . toHaveBeenCalledWith ( [ sseEvent ] ) ;
100
+ expect ( callbacks . onError ) . not . toHaveBeenCalled ( ) ;
101
+ expect ( callbacks . onUpdate ) . toHaveBeenCalledWith ( sseEvent ) ;
102
+ } ) ;
77
103
78
- const request = XRequest ( { baseURL, model } ) ;
79
- await request . create ( params , { onSuccess, onError, onUpdate } ) ;
80
-
81
- const sseEvent = { event : 'message' , data : '{"id":"0","content":"He"}' } ;
104
+ test ( 'should create request and handle custom response, e.g. application/x-ndjson' , async ( ) => {
105
+ mockedXFetch . mockResolvedValueOnce ( {
106
+ headers : {
107
+ get : jest . fn ( ) . mockReturnValue ( 'application/x-ndjson' ) ,
108
+ } ,
109
+ body : mockNdJsonReadableStream ( ) ,
110
+ } ) ;
111
+ await request . create ( params , callbacks , new TransformStream ( ) ) ;
112
+ expect ( callbacks . onSuccess ) . toHaveBeenCalledWith ( [
113
+ ndJsonData . split ( ND_JSON_SEPARATOR ) [ 0 ] ,
114
+ ndJsonData . split ( ND_JSON_SEPARATOR ) [ 1 ] ,
115
+ ] ) ;
116
+ expect ( callbacks . onError ) . not . toHaveBeenCalled ( ) ;
117
+ expect ( callbacks . onUpdate ) . toHaveBeenCalledWith ( ndJsonData . split ( ND_JSON_SEPARATOR ) [ 0 ] ) ;
118
+ expect ( callbacks . onUpdate ) . toHaveBeenCalledWith ( ndJsonData . split ( ND_JSON_SEPARATOR ) [ 1 ] ) ;
119
+ } ) ;
82
120
83
- expect ( onSuccess ) . toHaveBeenCalledWith ( [ sseEvent ] ) ;
84
- expect ( onError ) . not . toHaveBeenCalled ( ) ;
85
- expect ( onUpdate ) . toHaveBeenCalledWith ( sseEvent ) ;
121
+ test ( 'should reuse the same instance for the same baseURL or fetch' , ( ) => {
122
+ const request1 = XRequest ( options ) ;
123
+ const request2 = XRequest ( options ) ;
124
+ expect ( request1 ) . toBe ( request2 ) ;
125
+ const request3 = XRequest ( { fetch : mockedXFetch , baseURL : options . baseURL } ) ;
126
+ const request4 = XRequest ( { fetch : mockedXFetch , baseURL : options . baseURL } ) ;
127
+ expect ( request3 ) . toBe ( request4 ) ;
86
128
} ) ;
87
129
88
130
test ( 'should handle error response' , async ( ) => {
89
- const onSuccess = jest . fn ( ) ;
90
- const onError = jest . fn ( ) ;
91
- const onUpdate = jest . fn ( ) ;
92
- const params = { messages : [ { role : 'user' , content : 'Hello' } ] } ;
93
-
94
- const mockedXFetch = xFetch as jest . Mock ;
95
131
mockedXFetch . mockRejectedValueOnce ( new Error ( 'Fetch failed' ) ) ;
96
-
97
- const request = XRequest ( { baseURL, model } ) ;
98
- await request . create ( params , { onSuccess, onError, onUpdate } ) . catch ( ( ) => { } ) ;
99
-
100
- expect ( onSuccess ) . not . toHaveBeenCalled ( ) ;
101
- expect ( onError ) . toHaveBeenCalledWith ( new Error ( 'Fetch failed' ) ) ;
132
+ await request . create ( params , callbacks ) . catch ( ( ) => { } ) ;
133
+ expect ( callbacks . onSuccess ) . not . toHaveBeenCalled ( ) ;
134
+ expect ( callbacks . onError ) . toHaveBeenCalledWith ( new Error ( 'Fetch failed' ) ) ;
102
135
} ) ;
103
136
104
137
test ( 'should throw error for unsupported content type' , async ( ) => {
105
- const onSuccess = jest . fn ( ) ;
106
- const onError = jest . fn ( ) ;
107
- const onUpdate = jest . fn ( ) ;
108
- const params = { messages : [ { role : 'user' , content : 'Hello' } ] } ;
109
-
110
- const mockedXFetch = xFetch as jest . Mock ;
138
+ const contentType = 'text/plain' ;
111
139
mockedXFetch . mockResolvedValueOnce ( {
112
140
headers : {
113
- get : jest . fn ( ) . mockReturnValue ( 'text/plain' ) ,
141
+ get : jest . fn ( ) . mockReturnValue ( contentType ) ,
114
142
} ,
115
143
} ) ;
144
+ await request . create ( params , callbacks ) . catch ( ( ) => { } ) ;
145
+ expect ( callbacks . onSuccess ) . not . toHaveBeenCalled ( ) ;
146
+ expect ( callbacks . onError ) . toHaveBeenCalledWith (
147
+ new Error ( `The response content-type: ${ contentType } is not support!` ) ,
148
+ ) ;
149
+ } ) ;
116
150
117
- const request = XRequest ( { baseURL, model } ) ;
118
- await request . create ( params , { onSuccess, onError, onUpdate } ) . catch ( ( ) => { } ) ;
151
+ test ( 'should handle TransformStream errors' , async ( ) => {
152
+ const errorTransform = new TransformStream ( {
153
+ transform ( ) {
154
+ throw new Error ( 'Transform error' ) ;
155
+ } ,
156
+ } ) ;
119
157
120
- expect ( onSuccess ) . not . toHaveBeenCalled ( ) ;
121
- expect ( onError ) . toHaveBeenCalledWith (
122
- new Error ( 'The response content-type: text/plain is not support!' ) ,
123
- ) ;
158
+ mockedXFetch . mockResolvedValueOnce ( {
159
+ headers : {
160
+ get : jest . fn ( ) . mockReturnValue ( 'application/x-ndjson' ) ,
161
+ } ,
162
+ body : mockNdJsonReadableStream ( ) ,
163
+ } ) ;
164
+
165
+ await request . create ( params , callbacks , errorTransform ) . catch ( ( ) => { } ) ;
166
+ expect ( callbacks . onError ) . toHaveBeenCalledWith ( new Error ( 'Transform error' ) ) ;
167
+ expect ( callbacks . onSuccess ) . not . toHaveBeenCalled ( ) ;
168
+ expect ( callbacks . onUpdate ) . not . toHaveBeenCalled ( ) ;
124
169
} ) ;
125
170
} ) ;
0 commit comments