@@ -70,7 +70,9 @@ describe('OpenAIRealtimeWebSocket', () => {
70
70
const ws = new OpenAIRealtimeWebSocket ( ) ;
71
71
const audioSpy = vi . fn ( ) ;
72
72
ws . on ( 'audio' , audioSpy ) ;
73
- const sendSpy = vi . spyOn ( ws , 'sendEvent' ) ;
73
+ const sendSpy = vi
74
+ . spyOn ( ws as any , 'sendEvent' )
75
+ . mockImplementation ( ( ) => { } ) ;
74
76
const interruptSpy = vi . spyOn ( ws , 'interrupt' ) ;
75
77
const p = ws . connect ( { apiKey : 'ek' , model : 'm' } ) ;
76
78
await vi . runAllTimersAsync ( ) ;
@@ -109,11 +111,13 @@ describe('OpenAIRealtimeWebSocket', () => {
109
111
} ) ;
110
112
expect ( interruptSpy ) . toHaveBeenCalled ( ) ;
111
113
expect (
112
- sendSpy . mock . calls . some ( ( c ) => c [ 0 ] . type === 'response.cancel' ) ,
114
+ sendSpy . mock . calls . some (
115
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'response.cancel' ,
116
+ ) ,
113
117
) . toBe ( true ) ;
114
118
expect (
115
119
sendSpy . mock . calls . some (
116
- ( c ) => c [ 0 ] . type === 'conversation.item.truncate' ,
120
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
117
121
) ,
118
122
) . toBe ( true ) ;
119
123
@@ -147,7 +151,9 @@ describe('OpenAIRealtimeWebSocket', () => {
147
151
} ) ,
148
152
} ) ;
149
153
expect (
150
- sendSpy . mock . calls . every ( ( c ) => c [ 0 ] . type !== 'response.cancel' ) ,
154
+ sendSpy . mock . calls . every (
155
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type !== 'response.cancel' ,
156
+ ) ,
151
157
) . toBe ( true ) ;
152
158
} ) ;
153
159
@@ -158,7 +164,9 @@ describe('OpenAIRealtimeWebSocket', () => {
158
164
159
165
it ( 'close resets state so interrupt does nothing' , async ( ) => {
160
166
const ws = new OpenAIRealtimeWebSocket ( ) ;
161
- const sendSpy = vi . spyOn ( ws , 'sendEvent' ) ;
167
+ const sendSpy = vi
168
+ . spyOn ( OpenAIRealtimeWebSocket . prototype as any , 'sendEvent' )
169
+ . mockImplementation ( ( ) => { } ) ;
162
170
const p = ws . connect ( { apiKey : 'ek' , model : 'm' } ) ;
163
171
await vi . runAllTimersAsync ( ) ;
164
172
await p ;
@@ -197,6 +205,99 @@ describe('OpenAIRealtimeWebSocket', () => {
197
205
expect ( baseSpy ) . toHaveBeenCalled ( ) ;
198
206
} ) ;
199
207
208
+ it ( '_interrupt quantizes and clamps elapsedTime' , ( ) => {
209
+ const ws = new OpenAIRealtimeWebSocket ( ) ;
210
+ const sendSpy = vi
211
+ . spyOn ( OpenAIRealtimeWebSocket . prototype as any , 'sendEvent' )
212
+ . mockImplementation ( ( ) => { } ) ;
213
+ // @ts -expect-error - testing protected field.
214
+ ws . _audioLengthMs = 100 ;
215
+ ws . _interrupt ( 110.9 , false ) ;
216
+ let call = sendSpy . mock . calls . find (
217
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
218
+ ) ;
219
+ expect ( ( call ?. [ 0 ] as any ) . audio_end_ms ) . toBe ( 100 ) ;
220
+ sendSpy . mockClear ( ) ;
221
+ // @ts -expect-error - testing protected field.
222
+ ws . _audioLengthMs = 200 ;
223
+ ws . _interrupt ( 123.7 , false ) ;
224
+ call = sendSpy . mock . calls . find (
225
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
226
+ ) ;
227
+ expect ( ( call ?. [ 0 ] as any ) . audio_end_ms ) . toBe ( 123 ) ;
228
+ sendSpy . mockRestore ( ) ;
229
+ } ) ;
230
+
231
+ it ( '_interrupt floors sub-millisecond elapsedTime' , ( ) => {
232
+ const ws = new OpenAIRealtimeWebSocket ( ) ;
233
+ const sendSpy = vi
234
+ . spyOn ( OpenAIRealtimeWebSocket . prototype as any , 'sendEvent' )
235
+ . mockImplementation ( ( ) => { } ) ;
236
+ // @ts -expect-error - testing protected field.
237
+ ws . _audioLengthMs = 100 ;
238
+ ws . _interrupt ( 0.9 , false ) ;
239
+ const call = sendSpy . mock . calls . find (
240
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
241
+ ) ;
242
+ expect ( ( call ?. [ 0 ] as any ) . audio_end_ms ) . toBe ( 0 ) ;
243
+ expect ( Number . isInteger ( ( call ?. [ 0 ] as any ) . audio_end_ms ) ) . toBe ( true ) ;
244
+ sendSpy . mockRestore ( ) ;
245
+ } ) ;
246
+
247
+ it ( '_interrupt clamps overshoot elapsedTime' , ( ) => {
248
+ const ws = new OpenAIRealtimeWebSocket ( ) ;
249
+ const sendSpy = vi
250
+ . spyOn ( OpenAIRealtimeWebSocket . prototype as any , 'sendEvent' )
251
+ . mockImplementation ( ( ) => { } ) ;
252
+ // @ts -expect-error - testing protected field.
253
+ ws . _audioLengthMs = 42 ;
254
+ ws . _interrupt ( 42.6 , false ) ;
255
+ const call = sendSpy . mock . calls . find (
256
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
257
+ ) ;
258
+ expect ( ( call ?. [ 0 ] as any ) . audio_end_ms ) . toBe ( 42 ) ;
259
+ expect ( Number . isInteger ( ( call ?. [ 0 ] as any ) . audio_end_ms ) ) . toBe ( true ) ;
260
+ sendSpy . mockRestore ( ) ;
261
+ } ) ;
262
+
263
+ it ( 'interrupt payload is integer with fractional speed' , async ( ) => {
264
+ const ws = new OpenAIRealtimeWebSocket ( ) ;
265
+ const sendSpy = vi
266
+ . spyOn ( OpenAIRealtimeWebSocket . prototype as any , 'sendEvent' )
267
+ . mockImplementation ( ( ) => { } ) ;
268
+ const p = ws . connect ( {
269
+ apiKey : 'ek' ,
270
+ model : 'm' ,
271
+ initialSessionConfig : { speed : 1.1 } ,
272
+ } as any ) ;
273
+ await vi . runAllTimersAsync ( ) ;
274
+ await p ;
275
+ // @ts -expect-error - testing protected field.
276
+ ws . _audioLengthMs = 200 ;
277
+ ws . _interrupt ( 123.4 , false ) ;
278
+ const call = sendSpy . mock . calls . find (
279
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
280
+ ) ;
281
+ expect ( Number . isInteger ( ( call ?. [ 0 ] as any ) . audio_end_ms ) ) . toBe ( true ) ;
282
+ sendSpy . mockRestore ( ) ;
283
+ } ) ;
284
+
285
+ it ( 'interrupt payload is integer with speed 1' , ( ) => {
286
+ const ws = new OpenAIRealtimeWebSocket ( ) ;
287
+ const sendSpy = vi
288
+ . spyOn ( OpenAIRealtimeWebSocket . prototype as any , 'sendEvent' )
289
+ . mockImplementation ( ( ) => { } ) ;
290
+ // @ts -expect-error - testing protected field.
291
+ ws . _audioLengthMs = 200 ;
292
+ ws . _interrupt ( 123.4 , false ) ;
293
+ const call = sendSpy . mock . calls . find (
294
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
295
+ ) ;
296
+ expect ( ( call ?. [ 0 ] as any ) . audio_end_ms ) . toBe ( 123 ) ;
297
+ expect ( Number . isInteger ( ( call ?. [ 0 ] as any ) . audio_end_ms ) ) . toBe ( true ) ;
298
+ sendSpy . mockRestore ( ) ;
299
+ } ) ;
300
+
200
301
it ( 'full interrupt/_interrupt flow' , async ( ) => {
201
302
const ws = new OpenAIRealtimeWebSocket ( ) ;
202
303
const sendSpy = vi . spyOn ( ws , 'sendEvent' ) ;
@@ -230,11 +331,13 @@ describe('OpenAIRealtimeWebSocket', () => {
230
331
} ) ,
231
332
} ) ;
232
333
expect (
233
- sendSpy . mock . calls . some ( ( c ) => c [ 0 ] . type === 'response.cancel' ) ,
334
+ sendSpy . mock . calls . some (
335
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'response.cancel' ,
336
+ ) ,
234
337
) . toBe ( true ) ;
235
338
expect (
236
339
sendSpy . mock . calls . some (
237
- ( c ) => c [ 0 ] . type === 'conversation.item.truncate' ,
340
+ ( c : unknown [ ] ) => ( c [ 0 ] as any ) . type === 'conversation.item.truncate' ,
238
341
) ,
239
342
) . toBe ( true ) ;
240
343
sendSpy . mockClear ( ) ;
0 commit comments