@volley/recognition-client-sdk 0.1.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Unit tests for SimplifiedVGFRecognitionClient
3
+ */
4
+
5
+ import { SimplifiedVGFRecognitionClient, createSimplifiedVGFClient } from './simplified-vgf-recognition-client.js';
6
+ import { RealTimeTwoWayWebSocketRecognitionClient } from './recognition-client.js';
7
+ import { ClientState } from './recognition-client.types.js';
8
+ import { AudioEncoding, RecognitionContextTypeV1 } from '@recog/shared-types';
9
+ import {
10
+ RecordingStatus,
11
+ TranscriptionStatus,
12
+ type RecognitionState
13
+ } from './vgf-recognition-state.js';
14
+
15
+ // Mock the underlying client
16
+ jest.mock('./recognition-client');
17
+
18
+ describe('SimplifiedVGFRecognitionClient', () => {
19
+ let mockClient: jest.Mocked<RealTimeTwoWayWebSocketRecognitionClient>;
20
+ let simplifiedClient: SimplifiedVGFRecognitionClient;
21
+ let stateChangeCallback: jest.Mock;
22
+
23
+ beforeEach(() => {
24
+ // Reset mocks
25
+ jest.clearAllMocks();
26
+
27
+ // Create mock for underlying client
28
+ mockClient = {
29
+ connect: jest.fn().mockResolvedValue(undefined),
30
+ sendAudio: jest.fn(),
31
+ stopRecording: jest.fn().mockResolvedValue(undefined),
32
+ getAudioUtteranceId: jest.fn().mockReturnValue('test-uuid'),
33
+ getState: jest.fn().mockReturnValue(ClientState.INITIAL),
34
+ isConnected: jest.fn().mockReturnValue(false),
35
+ isConnecting: jest.fn().mockReturnValue(false),
36
+ isStopping: jest.fn().mockReturnValue(false),
37
+ isTranscriptionFinished: jest.fn().mockReturnValue(false),
38
+ isBufferOverflowing: jest.fn().mockReturnValue(false),
39
+ } as any;
40
+
41
+ // Mock the constructor
42
+ (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>)
43
+ .mockImplementation(() => mockClient as any);
44
+
45
+ stateChangeCallback = jest.fn();
46
+ });
47
+
48
+ describe('Constructor', () => {
49
+ it('should initialize with correct default VGF state', () => {
50
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
51
+ asrRequestConfig: {
52
+ provider: 'deepgram',
53
+ language: 'en',
54
+ sampleRate: 16000,
55
+ encoding: AudioEncoding.LINEAR16
56
+ },
57
+ onStateChange: stateChangeCallback
58
+ });
59
+
60
+ const state = simplifiedClient.getVGFState();
61
+ expect(state.audioUtteranceId).toBeDefined();
62
+ expect(state.startRecordingStatus).toBe(RecordingStatus.READY);
63
+ expect(state.transcriptionStatus).toBe(TranscriptionStatus.NOT_STARTED);
64
+ expect(state.pendingTranscript).toBe('');
65
+ });
66
+
67
+ it('should accept initial state and use its audioUtteranceId', () => {
68
+ const initialState: RecognitionState = {
69
+ audioUtteranceId: 'existing-session-id',
70
+ startRecordingStatus: RecordingStatus.FINISHED,
71
+ finalTranscript: 'Previous transcript',
72
+ pendingTranscript: '', // Required field
73
+ transcriptionStatus: TranscriptionStatus.FINALIZED
74
+ };
75
+
76
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
77
+ initialState,
78
+ onStateChange: stateChangeCallback
79
+ });
80
+
81
+ const state = simplifiedClient.getVGFState();
82
+ expect(state.audioUtteranceId).toBe('existing-session-id');
83
+ expect(state.finalTranscript).toBe('Previous transcript');
84
+
85
+ // Verify audioUtteranceId was passed to underlying client
86
+ const constructorCalls = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls;
87
+ expect(constructorCalls[0]?.[0]?.audioUtteranceId).toBe('existing-session-id');
88
+ });
89
+
90
+ it('should store ASR config as JSON string', () => {
91
+ const asrConfig = {
92
+ provider: 'deepgram' as const,
93
+ language: 'en',
94
+ model: 'nova-2',
95
+ sampleRate: 16000,
96
+ encoding: AudioEncoding.LINEAR16
97
+ };
98
+
99
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
100
+ asrRequestConfig: asrConfig,
101
+ onStateChange: stateChangeCallback
102
+ });
103
+
104
+ const state = simplifiedClient.getVGFState();
105
+ expect(state.asrConfig).toBe(JSON.stringify(asrConfig));
106
+ });
107
+ });
108
+
109
+ describe('State Management', () => {
110
+ beforeEach(() => {
111
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
112
+ asrRequestConfig: {
113
+ provider: 'deepgram',
114
+ language: 'en',
115
+ sampleRate: 16000,
116
+ encoding: AudioEncoding.LINEAR16
117
+ },
118
+ onStateChange: stateChangeCallback
119
+ });
120
+ });
121
+
122
+ it('should update state to RECORDING when sendAudio is called', () => {
123
+ const audioData = Buffer.from([1, 2, 3, 4]);
124
+ simplifiedClient.sendAudio(audioData);
125
+
126
+ expect(stateChangeCallback).toHaveBeenCalled();
127
+ const updatedState = stateChangeCallback.mock.calls[0][0];
128
+ expect(updatedState.startRecordingStatus).toBe(RecordingStatus.RECORDING);
129
+ expect(updatedState.startRecordingTimestamp).toBeDefined();
130
+ });
131
+
132
+ it('should only set recording timestamp once', () => {
133
+ const audioData = Buffer.from([1, 2, 3, 4]);
134
+
135
+ simplifiedClient.sendAudio(audioData);
136
+ const firstTimestamp = stateChangeCallback.mock.calls[0][0].startRecordingTimestamp;
137
+
138
+ // Clear mock to verify no additional state changes
139
+ stateChangeCallback.mockClear();
140
+
141
+ // Second sendAudio should not trigger state change since already recording
142
+ simplifiedClient.sendAudio(audioData);
143
+ expect(stateChangeCallback).not.toHaveBeenCalled();
144
+
145
+ // Verify timestamp hasn't changed in internal state
146
+ const currentState = simplifiedClient.getVGFState();
147
+ expect(currentState.startRecordingTimestamp).toBe(firstTimestamp);
148
+ });
149
+
150
+ it('should update state to FINISHED when stopRecording is called', async () => {
151
+ await simplifiedClient.stopRecording();
152
+
153
+ expect(stateChangeCallback).toHaveBeenCalled();
154
+ const updatedState = stateChangeCallback.mock.calls[0][0];
155
+ expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
156
+ expect(updatedState.finalRecordingTimestamp).toBeDefined();
157
+ });
158
+ });
159
+
160
+ describe('Transcript Callbacks', () => {
161
+ let onTranscriptCallback: (result: any) => void;
162
+ let onMetadataCallback: (metadata: any) => void;
163
+ let onErrorCallback: (error: any) => void;
164
+
165
+ beforeEach(() => {
166
+ // Capture the callbacks passed to underlying client
167
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
168
+ asrRequestConfig: {
169
+ provider: 'deepgram',
170
+ language: 'en',
171
+ sampleRate: 16000,
172
+ encoding: AudioEncoding.LINEAR16
173
+ },
174
+ onStateChange: stateChangeCallback
175
+ });
176
+
177
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
178
+ onTranscriptCallback = constructorCall?.onTranscript ?? jest.fn();
179
+ onMetadataCallback = constructorCall?.onMetadata ?? jest.fn();
180
+ onErrorCallback = constructorCall?.onError ?? jest.fn();
181
+ });
182
+
183
+ it('should directly copy pending transcript without combining', () => {
184
+ const transcriptResult = {
185
+ finalTranscript: 'Hello',
186
+ pendingTranscript: ' world',
187
+ pendingTranscriptConfidence: 0.85,
188
+ finalTranscriptConfidence: 0.95,
189
+ is_finished: false
190
+ };
191
+
192
+ onTranscriptCallback(transcriptResult);
193
+
194
+ expect(stateChangeCallback).toHaveBeenCalled();
195
+ const updatedState = stateChangeCallback.mock.calls[0][0];
196
+ expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.IN_PROGRESS);
197
+ // Should be direct copy, NOT combined
198
+ expect(updatedState.pendingTranscript).toBe(' world');
199
+ expect(updatedState.pendingConfidence).toBe(0.85);
200
+ // Final should also be copied when present
201
+ expect(updatedState.finalTranscript).toBe('Hello');
202
+ expect(updatedState.finalConfidence).toBe(0.95);
203
+ });
204
+
205
+ it('should update VGF state with final transcript', () => {
206
+ const transcriptResult = {
207
+ finalTranscript: 'Hello world',
208
+ finalTranscriptConfidence: 0.98,
209
+ is_finished: true
210
+ };
211
+
212
+ onTranscriptCallback(transcriptResult);
213
+
214
+ expect(stateChangeCallback).toHaveBeenCalled();
215
+ const updatedState = stateChangeCallback.mock.calls[0][0];
216
+ expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.FINALIZED);
217
+ expect(updatedState.finalTranscript).toBe('Hello world');
218
+ expect(updatedState.finalConfidence).toBe(0.98);
219
+ expect(updatedState.pendingTranscript).toBe('');
220
+ expect(updatedState.finalTranscriptionTimestamp).toBeDefined();
221
+ });
222
+
223
+ it('should handle metadata and mark recording as finished', () => {
224
+ const metadata = {
225
+ audioUtteranceId: 'test-uuid',
226
+ duration: 5000
227
+ };
228
+
229
+ onMetadataCallback(metadata);
230
+
231
+ expect(stateChangeCallback).toHaveBeenCalled();
232
+ const updatedState = stateChangeCallback.mock.calls[0][0];
233
+ expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
234
+ expect(updatedState.finalRecordingTimestamp).toBeDefined();
235
+ });
236
+
237
+ it('should handle errors and update state', () => {
238
+ const error = {
239
+ message: 'Recognition failed',
240
+ code: 'RECOGNITION_ERROR'
241
+ };
242
+
243
+ onErrorCallback(error);
244
+
245
+ expect(stateChangeCallback).toHaveBeenCalled();
246
+ const updatedState = stateChangeCallback.mock.calls[0][0];
247
+ expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.ERROR);
248
+ expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
249
+ });
250
+
251
+ it('should reset isRecordingAudio on error', () => {
252
+ // First start recording
253
+ simplifiedClient.sendAudio(Buffer.from([1, 2, 3]));
254
+
255
+ // Then error occurs
256
+ onErrorCallback({ message: 'Error' });
257
+
258
+ // Send audio again should restart recording
259
+ stateChangeCallback.mockClear();
260
+ simplifiedClient.sendAudio(Buffer.from([4, 5, 6]));
261
+
262
+ const updatedState = stateChangeCallback.mock.calls[0][0];
263
+ expect(updatedState.startRecordingStatus).toBe(RecordingStatus.RECORDING);
264
+ expect(updatedState.startRecordingTimestamp).toBeDefined();
265
+ });
266
+ });
267
+
268
+ describe('Method Delegation', () => {
269
+ beforeEach(() => {
270
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
271
+ asrRequestConfig: {
272
+ provider: 'deepgram',
273
+ language: 'en',
274
+ sampleRate: 16000,
275
+ encoding: AudioEncoding.LINEAR16
276
+ }
277
+ });
278
+ });
279
+
280
+ it('should delegate connect() to underlying client', async () => {
281
+ await simplifiedClient.connect();
282
+ expect(mockClient.connect).toHaveBeenCalled();
283
+ });
284
+
285
+ it('should delegate sendAudio() to underlying client', () => {
286
+ const audioData = Buffer.from([1, 2, 3, 4]);
287
+ simplifiedClient.sendAudio(audioData);
288
+ expect(mockClient.sendAudio).toHaveBeenCalledWith(audioData);
289
+ });
290
+
291
+ it('should delegate stopRecording() to underlying client', async () => {
292
+ await simplifiedClient.stopRecording();
293
+ expect(mockClient.stopRecording).toHaveBeenCalled();
294
+ });
295
+
296
+ it('should delegate status check methods', () => {
297
+ simplifiedClient.isConnected();
298
+ expect(mockClient.isConnected).toHaveBeenCalled();
299
+
300
+ simplifiedClient.isConnecting();
301
+ expect(mockClient.isConnecting).toHaveBeenCalled();
302
+
303
+ simplifiedClient.isStopping();
304
+ expect(mockClient.isStopping).toHaveBeenCalled();
305
+
306
+ simplifiedClient.isTranscriptionFinished();
307
+ expect(mockClient.isTranscriptionFinished).toHaveBeenCalled();
308
+
309
+ simplifiedClient.isBufferOverflowing();
310
+ expect(mockClient.isBufferOverflowing).toHaveBeenCalled();
311
+ });
312
+
313
+ it('should delegate getAudioUtteranceId()', () => {
314
+ const id = simplifiedClient.getAudioUtteranceId();
315
+ expect(mockClient.getAudioUtteranceId).toHaveBeenCalled();
316
+ expect(id).toBe('test-uuid');
317
+ });
318
+
319
+ it('should delegate getState()', () => {
320
+ const state = simplifiedClient.getState();
321
+ expect(mockClient.getState).toHaveBeenCalled();
322
+ expect(state).toBe(ClientState.INITIAL);
323
+ });
324
+ });
325
+
326
+ describe('Original Callbacks', () => {
327
+ it('should call original onTranscript callback if provided', () => {
328
+ const originalOnTranscript = jest.fn();
329
+
330
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
331
+ asrRequestConfig: {
332
+ provider: 'deepgram',
333
+ language: 'en',
334
+ sampleRate: 16000,
335
+ encoding: AudioEncoding.LINEAR16
336
+ },
337
+ onTranscript: originalOnTranscript
338
+ });
339
+
340
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
341
+ const wrappedCallback = constructorCall?.onTranscript;
342
+ if (!wrappedCallback) throw new Error('onTranscript callback not found');
343
+
344
+ const result = { finalTranscript: 'test', is_finished: false };
345
+ wrappedCallback(result as any);
346
+
347
+ expect(originalOnTranscript).toHaveBeenCalledWith(result);
348
+ });
349
+
350
+ it('should call original onError callback if provided', () => {
351
+ const originalOnError = jest.fn();
352
+
353
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
354
+ asrRequestConfig: {
355
+ provider: 'deepgram',
356
+ language: 'en',
357
+ sampleRate: 16000,
358
+ encoding: AudioEncoding.LINEAR16
359
+ },
360
+ onError: originalOnError
361
+ });
362
+
363
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
364
+ const wrappedCallback = constructorCall?.onError;
365
+ if (!wrappedCallback) throw new Error('onError callback not found');
366
+
367
+ const error = { message: 'test error' };
368
+ wrappedCallback(error as any);
369
+
370
+ expect(originalOnError).toHaveBeenCalledWith(error);
371
+ });
372
+ });
373
+
374
+ describe('Thin Layer Verification', () => {
375
+ it('should pass transcript result directly to mapper without modification', () => {
376
+ const transcriptResult = {
377
+ type: 'Transcription',
378
+ audioUtteranceId: 'test-123',
379
+ finalTranscript: 'Final text',
380
+ pendingTranscript: 'Pending text',
381
+ finalTranscriptConfidence: 0.99,
382
+ pendingTranscriptConfidence: 0.88,
383
+ is_finished: false,
384
+ voiceStart: 100,
385
+ voiceDuration: 500,
386
+ voiceEnd: 600,
387
+ extraField: 'should be ignored by VGF state'
388
+ };
389
+
390
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
391
+ asrRequestConfig: {
392
+ provider: 'deepgram',
393
+ language: 'en',
394
+ sampleRate: 16000,
395
+ encoding: AudioEncoding.LINEAR16
396
+ },
397
+ onStateChange: stateChangeCallback
398
+ });
399
+
400
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
401
+ const wrappedCallback = constructorCall?.onTranscript;
402
+ if (!wrappedCallback) throw new Error('onTranscript callback not found');
403
+
404
+ wrappedCallback(transcriptResult as any);
405
+
406
+ const updatedState = stateChangeCallback.mock.calls[0][0];
407
+ // Verify direct copy without modification
408
+ expect(updatedState.pendingTranscript).toBe('Pending text');
409
+ expect(updatedState.finalTranscript).toBe('Final text');
410
+ expect(updatedState.pendingConfidence).toBe(0.88);
411
+ expect(updatedState.finalConfidence).toBe(0.99);
412
+ });
413
+
414
+ it('should handle only pending transcript correctly', () => {
415
+ const transcriptResult = {
416
+ pendingTranscript: 'Just pending',
417
+ pendingTranscriptConfidence: 0.75,
418
+ is_finished: false
419
+ };
420
+
421
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
422
+ asrRequestConfig: {
423
+ provider: 'deepgram',
424
+ language: 'en',
425
+ sampleRate: 16000,
426
+ encoding: AudioEncoding.LINEAR16
427
+ },
428
+ onStateChange: stateChangeCallback
429
+ });
430
+
431
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
432
+ const wrappedCallback = constructorCall?.onTranscript;
433
+ if (!wrappedCallback) throw new Error('onTranscript callback not found');
434
+
435
+ wrappedCallback(transcriptResult as any);
436
+
437
+ const updatedState = stateChangeCallback.mock.calls[0][0];
438
+ expect(updatedState.pendingTranscript).toBe('Just pending');
439
+ expect(updatedState.pendingConfidence).toBe(0.75);
440
+ expect(updatedState.finalTranscript).toBeUndefined();
441
+ expect(updatedState.finalConfidence).toBeUndefined();
442
+ });
443
+
444
+ it('should handle only final transcript correctly', () => {
445
+ const transcriptResult = {
446
+ finalTranscript: 'Just final',
447
+ finalTranscriptConfidence: 0.92,
448
+ is_finished: false
449
+ };
450
+
451
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
452
+ asrRequestConfig: {
453
+ provider: 'deepgram',
454
+ language: 'en',
455
+ sampleRate: 16000,
456
+ encoding: AudioEncoding.LINEAR16
457
+ },
458
+ onStateChange: stateChangeCallback
459
+ });
460
+
461
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
462
+ const wrappedCallback = constructorCall?.onTranscript;
463
+ if (!wrappedCallback) throw new Error('onTranscript callback not found');
464
+
465
+ wrappedCallback(transcriptResult as any);
466
+
467
+ const updatedState = stateChangeCallback.mock.calls[0][0];
468
+ expect(updatedState.pendingTranscript).toBe(''); // Empty string when undefined
469
+ expect(updatedState.pendingConfidence).toBeUndefined();
470
+ expect(updatedState.finalTranscript).toBe('Just final');
471
+ expect(updatedState.finalConfidence).toBe(0.92);
472
+ });
473
+
474
+ it('should clear pending when is_finished is true', () => {
475
+ const transcriptResult = {
476
+ finalTranscript: 'Complete transcript',
477
+ pendingTranscript: 'Should be ignored',
478
+ finalTranscriptConfidence: 0.98,
479
+ pendingTranscriptConfidence: 0.77,
480
+ is_finished: true
481
+ };
482
+
483
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
484
+ asrRequestConfig: {
485
+ provider: 'deepgram',
486
+ language: 'en',
487
+ sampleRate: 16000,
488
+ encoding: AudioEncoding.LINEAR16
489
+ },
490
+ onStateChange: stateChangeCallback
491
+ });
492
+
493
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
494
+ const wrappedCallback = constructorCall?.onTranscript;
495
+ if (!wrappedCallback) throw new Error('onTranscript callback not found');
496
+
497
+ wrappedCallback(transcriptResult as any);
498
+
499
+ const updatedState = stateChangeCallback.mock.calls[0][0];
500
+ expect(updatedState.finalTranscript).toBe('Complete transcript');
501
+ expect(updatedState.finalConfidence).toBe(0.98);
502
+ // Pending should be cleared when finished
503
+ expect(updatedState.pendingTranscript).toBe('');
504
+ expect(updatedState.pendingConfidence).toBeUndefined();
505
+ expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.FINALIZED);
506
+ });
507
+ });
508
+
509
+ describe('Factory Function', () => {
510
+ it('should create SimplifiedVGFRecognitionClient instance', () => {
511
+ const client = createSimplifiedVGFClient({
512
+ asrRequestConfig: {
513
+ provider: 'deepgram',
514
+ language: 'en',
515
+ sampleRate: 16000,
516
+ encoding: AudioEncoding.LINEAR16
517
+ }
518
+ });
519
+
520
+ expect(client).toBeInstanceOf(SimplifiedVGFRecognitionClient);
521
+ expect(client.getVGFState).toBeDefined();
522
+ expect(client.connect).toBeDefined();
523
+ });
524
+ });
525
+
526
+ describe('PromptSlotMap Integration', () => {
527
+ it('should pass promptSlotMap from initial state to gameContext', () => {
528
+ const initialState: RecognitionState = {
529
+ audioUtteranceId: 'test-123',
530
+ pendingTranscript: '', // Required field
531
+ promptSlotMap: {
532
+ 'entity1': ['value1', 'value2'],
533
+ 'entity2': ['value3']
534
+ }
535
+ };
536
+
537
+ const gameContext = {
538
+ type: RecognitionContextTypeV1.GAME_CONTEXT,
539
+ gameId: 'test-game',
540
+ gamePhase: 'test-phase'
541
+ } as const;
542
+
543
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
544
+ asrRequestConfig: {
545
+ provider: 'deepgram',
546
+ language: 'en',
547
+ sampleRate: 16000,
548
+ encoding: AudioEncoding.LINEAR16
549
+ },
550
+ gameContext,
551
+ initialState
552
+ });
553
+
554
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
555
+
556
+ // Verify useContext was set to true
557
+ expect(constructorCall?.asrRequestConfig?.useContext).toBe(true);
558
+
559
+ // Verify slotMap was added to gameContext
560
+ expect(constructorCall?.gameContext?.slotMap).toEqual({
561
+ 'entity1': ['value1', 'value2'],
562
+ 'entity2': ['value3']
563
+ });
564
+ });
565
+
566
+ it('should warn if promptSlotMap exists but no gameContext provided', () => {
567
+ const logger = jest.fn();
568
+ const initialState: RecognitionState = {
569
+ audioUtteranceId: 'test-123',
570
+ pendingTranscript: '', // Required field
571
+ promptSlotMap: {
572
+ 'entity1': ['value1']
573
+ }
574
+ };
575
+
576
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
577
+ asrRequestConfig: {
578
+ provider: 'deepgram',
579
+ language: 'en',
580
+ sampleRate: 16000,
581
+ encoding: AudioEncoding.LINEAR16
582
+ },
583
+ initialState,
584
+ logger
585
+ });
586
+
587
+ expect(logger).toHaveBeenCalledWith(
588
+ 'warn',
589
+ '[VGF] promptSlotMap found but no gameContext provided. SlotMap will not be sent.'
590
+ );
591
+ });
592
+
593
+ it('should preserve promptSlotMap throughout state changes', () => {
594
+ const initialState: RecognitionState = {
595
+ audioUtteranceId: 'test-123',
596
+ pendingTranscript: '', // Required field
597
+ promptSlotMap: {
598
+ 'slots': ['test']
599
+ }
600
+ };
601
+
602
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
603
+ asrRequestConfig: {
604
+ provider: 'deepgram',
605
+ language: 'en',
606
+ sampleRate: 16000,
607
+ encoding: AudioEncoding.LINEAR16
608
+ },
609
+ initialState,
610
+ onStateChange: stateChangeCallback
611
+ });
612
+
613
+ // Send audio and verify promptSlotMap is preserved
614
+ simplifiedClient.sendAudio(Buffer.from([1, 2, 3]));
615
+
616
+ let state = stateChangeCallback.mock.calls[0][0];
617
+ expect(state.promptSlotMap).toEqual({ 'slots': ['test'] });
618
+
619
+ // Simulate transcript and verify preservation
620
+ const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
621
+ const onTranscriptCallback = constructorCall?.onTranscript;
622
+ if (!onTranscriptCallback) throw new Error('onTranscript callback not found');
623
+
624
+ onTranscriptCallback({
625
+ finalTranscript: 'test',
626
+ is_finished: false
627
+ } as any);
628
+
629
+ state = stateChangeCallback.mock.calls[1][0];
630
+ expect(state.promptSlotMap).toEqual({ 'slots': ['test'] });
631
+ });
632
+ });
633
+
634
+ describe('State Immutability', () => {
635
+ it('should return a copy of VGF state, not a reference', () => {
636
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
637
+ asrRequestConfig: {
638
+ provider: 'deepgram',
639
+ language: 'en',
640
+ sampleRate: 16000,
641
+ encoding: AudioEncoding.LINEAR16
642
+ }
643
+ });
644
+
645
+ const state1 = simplifiedClient.getVGFState();
646
+ const state2 = simplifiedClient.getVGFState();
647
+
648
+ expect(state1).not.toBe(state2); // Different object references
649
+ expect(state1).toEqual(state2); // But same content
650
+ });
651
+
652
+ it('should pass a copy of state to onStateChange callback', () => {
653
+ simplifiedClient = new SimplifiedVGFRecognitionClient({
654
+ asrRequestConfig: {
655
+ provider: 'deepgram',
656
+ language: 'en',
657
+ sampleRate: 16000,
658
+ encoding: AudioEncoding.LINEAR16
659
+ },
660
+ onStateChange: stateChangeCallback
661
+ });
662
+
663
+ simplifiedClient.sendAudio(Buffer.from([1, 2, 3]));
664
+
665
+ const callbackState = stateChangeCallback.mock.calls[0][0];
666
+ const currentState = simplifiedClient.getVGFState();
667
+
668
+ expect(callbackState).not.toBe(currentState); // Different references
669
+ });
670
+ });
671
+ });