@volley/recognition-client-sdk-node22 0.1.424

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.
Files changed (58) hide show
  1. package/README.md +344 -0
  2. package/dist/browser.bundled.d.ts +1280 -0
  3. package/dist/browser.d.ts +10 -0
  4. package/dist/browser.d.ts.map +1 -0
  5. package/dist/config-builder.d.ts +134 -0
  6. package/dist/config-builder.d.ts.map +1 -0
  7. package/dist/errors.d.ts +41 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/factory.d.ts +36 -0
  10. package/dist/factory.d.ts.map +1 -0
  11. package/dist/index.bundled.d.ts +2572 -0
  12. package/dist/index.d.ts +16 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +10199 -0
  15. package/dist/index.js.map +7 -0
  16. package/dist/recog-client-sdk.browser.d.ts +10 -0
  17. package/dist/recog-client-sdk.browser.d.ts.map +1 -0
  18. package/dist/recog-client-sdk.browser.js +5746 -0
  19. package/dist/recog-client-sdk.browser.js.map +7 -0
  20. package/dist/recognition-client.d.ts +128 -0
  21. package/dist/recognition-client.d.ts.map +1 -0
  22. package/dist/recognition-client.types.d.ts +271 -0
  23. package/dist/recognition-client.types.d.ts.map +1 -0
  24. package/dist/simplified-vgf-recognition-client.d.ts +178 -0
  25. package/dist/simplified-vgf-recognition-client.d.ts.map +1 -0
  26. package/dist/utils/audio-ring-buffer.d.ts +69 -0
  27. package/dist/utils/audio-ring-buffer.d.ts.map +1 -0
  28. package/dist/utils/message-handler.d.ts +45 -0
  29. package/dist/utils/message-handler.d.ts.map +1 -0
  30. package/dist/utils/url-builder.d.ts +28 -0
  31. package/dist/utils/url-builder.d.ts.map +1 -0
  32. package/dist/vgf-recognition-mapper.d.ts +66 -0
  33. package/dist/vgf-recognition-mapper.d.ts.map +1 -0
  34. package/dist/vgf-recognition-state.d.ts +91 -0
  35. package/dist/vgf-recognition-state.d.ts.map +1 -0
  36. package/package.json +74 -0
  37. package/src/browser.ts +24 -0
  38. package/src/config-builder.spec.ts +265 -0
  39. package/src/config-builder.ts +240 -0
  40. package/src/errors.ts +84 -0
  41. package/src/factory.spec.ts +215 -0
  42. package/src/factory.ts +47 -0
  43. package/src/index.ts +127 -0
  44. package/src/recognition-client.spec.ts +889 -0
  45. package/src/recognition-client.ts +844 -0
  46. package/src/recognition-client.types.ts +338 -0
  47. package/src/simplified-vgf-recognition-client.integration.spec.ts +718 -0
  48. package/src/simplified-vgf-recognition-client.spec.ts +1525 -0
  49. package/src/simplified-vgf-recognition-client.ts +524 -0
  50. package/src/utils/audio-ring-buffer.spec.ts +335 -0
  51. package/src/utils/audio-ring-buffer.ts +170 -0
  52. package/src/utils/message-handler.spec.ts +311 -0
  53. package/src/utils/message-handler.ts +131 -0
  54. package/src/utils/url-builder.spec.ts +252 -0
  55. package/src/utils/url-builder.ts +92 -0
  56. package/src/vgf-recognition-mapper.spec.ts +78 -0
  57. package/src/vgf-recognition-mapper.ts +232 -0
  58. package/src/vgf-recognition-state.ts +102 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Unit tests for MessageHandler
3
+ */
4
+
5
+ import { MessageHandler, MessageHandlerCallbacks } from './message-handler.js';
6
+ import { RecognitionResultTypeV1, ClientControlActionV1 } from '@recog/shared-types';
7
+
8
+ describe('MessageHandler', () => {
9
+ let callbacks: MessageHandlerCallbacks;
10
+ let handler: MessageHandler;
11
+ let mockLogger: jest.Mock;
12
+
13
+ beforeEach(() => {
14
+ mockLogger = jest.fn();
15
+ callbacks = {
16
+ onTranscript: jest.fn(),
17
+ onFunctionCall: jest.fn(),
18
+ onMetadata: jest.fn(),
19
+ onError: jest.fn(),
20
+ onControlMessage: jest.fn(),
21
+ logger: mockLogger
22
+ };
23
+ handler = new MessageHandler(callbacks);
24
+ });
25
+
26
+ describe('handleMessage', () => {
27
+ it('should handle transcription message', () => {
28
+ const msg = {
29
+ v: 1,
30
+ type: 'recognition_result',
31
+ data: {
32
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
33
+ transcript: 'hello world',
34
+ isFinal: true
35
+ }
36
+ };
37
+
38
+ handler.handleMessage(msg);
39
+ expect(callbacks.onTranscript).toHaveBeenCalledWith(msg.data);
40
+ });
41
+
42
+ it('should handle function call message', () => {
43
+ const msg = {
44
+ v: 1,
45
+ type: 'recognition_result',
46
+ data: {
47
+ type: RecognitionResultTypeV1.FUNCTION_CALL,
48
+ functionName: 'testFunction',
49
+ arguments: { arg1: 'value1' }
50
+ }
51
+ };
52
+
53
+ handler.handleMessage(msg);
54
+ expect(callbacks.onFunctionCall).toHaveBeenCalledWith(msg.data);
55
+ });
56
+
57
+ it('should handle metadata message', () => {
58
+ const msg = {
59
+ v: 1,
60
+ type: 'recognition_result',
61
+ data: {
62
+ type: RecognitionResultTypeV1.METADATA,
63
+ metadata: { key: 'value' }
64
+ }
65
+ };
66
+
67
+ handler.handleMessage(msg);
68
+ expect(callbacks.onMetadata).toHaveBeenCalledWith(msg.data);
69
+ });
70
+
71
+ it('should handle error message', () => {
72
+ const msg = {
73
+ v: 1,
74
+ type: 'recognition_result',
75
+ data: {
76
+ type: RecognitionResultTypeV1.ERROR,
77
+ error: 'test error',
78
+ code: 'TEST_ERROR'
79
+ }
80
+ };
81
+
82
+ handler.handleMessage(msg);
83
+ expect(callbacks.onError).toHaveBeenCalledWith(msg.data);
84
+ });
85
+
86
+ it('should handle client control message', () => {
87
+ const msg = {
88
+ v: 1,
89
+ type: 'recognition_result',
90
+ data: {
91
+ type: RecognitionResultTypeV1.CLIENT_CONTROL_MESSAGE,
92
+ action: ClientControlActionV1.STOP_RECORDING,
93
+ audioUtteranceId: 'test-utterance'
94
+ }
95
+ };
96
+
97
+ handler.handleMessage(msg);
98
+ expect(callbacks.onControlMessage).toHaveBeenCalledWith(msg.data);
99
+ });
100
+
101
+ it('should handle unknown message type', () => {
102
+ const msg = {
103
+ v: 1,
104
+ type: 'unknown_type',
105
+ data: {
106
+ type: 'unknown',
107
+ content: 'test'
108
+ }
109
+ };
110
+
111
+ handler.handleMessage(msg);
112
+ expect(mockLogger).toHaveBeenCalledWith(
113
+ 'debug',
114
+ '[RecogSDK] Unknown message type',
115
+ expect.objectContaining({ type: 'unknown' })
116
+ );
117
+ });
118
+
119
+ it('should log all incoming messages', () => {
120
+ const msg = {
121
+ v: 1,
122
+ type: 'recognition_result',
123
+ data: {
124
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
125
+ transcript: 'test'
126
+ }
127
+ };
128
+
129
+ handler.handleMessage(msg);
130
+ expect(mockLogger).toHaveBeenCalledWith(
131
+ 'debug',
132
+ '[RecogSDK] Received WebSocket message',
133
+ expect.objectContaining({
134
+ msgType: 'recognition_result',
135
+ msgDataType: RecognitionResultTypeV1.TRANSCRIPTION
136
+ })
137
+ );
138
+ });
139
+
140
+ it('should handle primitive msg.data', () => {
141
+ const msg = {
142
+ v: 1,
143
+ type: 'recognition_result',
144
+ data: 'primitive string'
145
+ };
146
+
147
+ handler.handleMessage(msg);
148
+ expect(mockLogger).toHaveBeenCalledWith(
149
+ 'error',
150
+ '[RecogSDK] Received primitive msg.data from server',
151
+ expect.objectContaining({
152
+ dataType: 'string',
153
+ data: 'primitive string'
154
+ })
155
+ );
156
+ });
157
+
158
+ it('should handle message without data field', () => {
159
+ const msg = {
160
+ v: 1,
161
+ type: RecognitionResultTypeV1.METADATA,
162
+ data: {
163
+ type: RecognitionResultTypeV1.METADATA,
164
+ metadata: { key: 'value' }
165
+ }
166
+ };
167
+
168
+ handler.handleMessage(msg);
169
+ expect(callbacks.onMetadata).toHaveBeenCalled();
170
+ });
171
+
172
+ it('should work without logger', () => {
173
+ const callbacksNoLogger = {
174
+ onTranscript: jest.fn(),
175
+ onFunctionCall: jest.fn(),
176
+ onMetadata: jest.fn(),
177
+ onError: jest.fn(),
178
+ onControlMessage: jest.fn()
179
+ };
180
+ const handlerNoLogger = new MessageHandler(callbacksNoLogger);
181
+
182
+ const msg = {
183
+ v: 1,
184
+ type: 'recognition_result',
185
+ data: {
186
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
187
+ transcript: 'test'
188
+ }
189
+ };
190
+
191
+ expect(() => handlerNoLogger.handleMessage(msg)).not.toThrow();
192
+ expect(callbacksNoLogger.onTranscript).toHaveBeenCalled();
193
+ });
194
+ });
195
+
196
+ describe('setSessionStartTime', () => {
197
+ it('should set session start time', () => {
198
+ const startTime = Date.now();
199
+ handler.setSessionStartTime(startTime);
200
+
201
+ const metrics = handler.getMetrics();
202
+ expect(metrics.sessionStartTime).toBe(startTime);
203
+ });
204
+ });
205
+
206
+ describe('getMetrics', () => {
207
+ it('should return initial metrics', () => {
208
+ const metrics = handler.getMetrics();
209
+ expect(metrics).toEqual({
210
+ sessionStartTime: null,
211
+ firstTranscriptTime: null,
212
+ timeToFirstTranscript: null
213
+ });
214
+ });
215
+
216
+ it('should track time to first transcript', () => {
217
+ const startTime = Date.now();
218
+ handler.setSessionStartTime(startTime);
219
+
220
+ // Simulate delay before first transcript
221
+ const transcriptMsg = {
222
+ v: 1,
223
+ type: 'recognition_result',
224
+ data: {
225
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
226
+ transcript: 'first transcript',
227
+ isFinal: false
228
+ }
229
+ };
230
+
231
+ handler.handleMessage(transcriptMsg);
232
+
233
+ const metrics = handler.getMetrics();
234
+ expect(metrics.sessionStartTime).toBe(startTime);
235
+ expect(metrics.firstTranscriptTime).toBeGreaterThanOrEqual(startTime);
236
+ expect(metrics.timeToFirstTranscript).toBeGreaterThanOrEqual(0);
237
+ });
238
+
239
+ it('should log time to first transcript', () => {
240
+ const startTime = Date.now();
241
+ handler.setSessionStartTime(startTime);
242
+
243
+ const transcriptMsg = {
244
+ v: 1,
245
+ type: 'recognition_result',
246
+ data: {
247
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
248
+ transcript: 'first transcript'
249
+ }
250
+ };
251
+
252
+ handler.handleMessage(transcriptMsg);
253
+
254
+ expect(mockLogger).toHaveBeenCalledWith(
255
+ 'debug',
256
+ '[RecogSDK] First transcript received',
257
+ expect.objectContaining({
258
+ timeToFirstTranscriptMs: expect.any(Number)
259
+ })
260
+ );
261
+ });
262
+
263
+ it('should only track first transcript time once', () => {
264
+ const startTime = Date.now();
265
+ handler.setSessionStartTime(startTime);
266
+
267
+ // First transcript
268
+ handler.handleMessage({
269
+ v: 1,
270
+ type: 'recognition_result',
271
+ data: {
272
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
273
+ transcript: 'first'
274
+ }
275
+ });
276
+
277
+ const firstMetrics = handler.getMetrics();
278
+ const firstTranscriptTime = firstMetrics.firstTranscriptTime;
279
+
280
+ // Second transcript
281
+ handler.handleMessage({
282
+ v: 1,
283
+ type: 'recognition_result',
284
+ data: {
285
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
286
+ transcript: 'second'
287
+ }
288
+ });
289
+
290
+ const secondMetrics = handler.getMetrics();
291
+ expect(secondMetrics.firstTranscriptTime).toBe(firstTranscriptTime);
292
+ });
293
+
294
+ it('should not track transcript time without session start', () => {
295
+ const transcriptMsg = {
296
+ v: 1,
297
+ type: 'recognition_result',
298
+ data: {
299
+ type: RecognitionResultTypeV1.TRANSCRIPTION,
300
+ transcript: 'test'
301
+ }
302
+ };
303
+
304
+ handler.handleMessage(transcriptMsg);
305
+
306
+ const metrics = handler.getMetrics();
307
+ expect(metrics.firstTranscriptTime).toBeNull();
308
+ expect(metrics.timeToFirstTranscript).toBeNull();
309
+ });
310
+ });
311
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Message Handler for Recognition Client
3
+ * Routes incoming WebSocket messages to appropriate callbacks
4
+ */
5
+
6
+ import {
7
+ RecognitionResultTypeV1,
8
+ ClientControlActionV1,
9
+ type TranscriptionResultV1,
10
+ type FunctionCallResultV1,
11
+ type MetadataResultV1,
12
+ type ErrorResultV1,
13
+ type ClientControlMessageV1
14
+ } from '@recog/shared-types';
15
+
16
+ export interface MessageHandlerCallbacks {
17
+ onTranscript: (result: TranscriptionResultV1) => void;
18
+ onFunctionCall: (result: FunctionCallResultV1) => void;
19
+ onMetadata: (metadata: MetadataResultV1) => void;
20
+ onError: (error: ErrorResultV1) => void;
21
+ onControlMessage: (msg: ClientControlMessageV1) => void;
22
+ logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any) => void;
23
+ }
24
+
25
+ export class MessageHandler {
26
+ private firstTranscriptTime: number | null = null;
27
+ private sessionStartTime: number | null = null;
28
+ private callbacks: MessageHandlerCallbacks;
29
+
30
+ constructor(callbacks: MessageHandlerCallbacks) {
31
+ this.callbacks = callbacks;
32
+ }
33
+
34
+ /**
35
+ * Set session start time for performance tracking
36
+ */
37
+ setSessionStartTime(time: number): void {
38
+ this.sessionStartTime = time;
39
+ }
40
+
41
+ /**
42
+ * Handle incoming WebSocket message
43
+ */
44
+ handleMessage(msg: { v: number; type: string; data: any }): void {
45
+ // Log ALL incoming messages for debugging
46
+ if (this.callbacks.logger) {
47
+ this.callbacks.logger('debug', '[RecogSDK] Received WebSocket message', {
48
+ msgType: msg.type,
49
+ msgDataType: msg.data && typeof msg.data === 'object' && 'type' in msg.data ? msg.data.type : 'N/A',
50
+ fullMessage: msg
51
+ });
52
+ }
53
+
54
+ // Safely check for type in msg.data - guard against primitives
55
+ // Log error if we receive primitive data (indicates server issue)
56
+ if (msg.data && typeof msg.data !== 'object') {
57
+ if (this.callbacks.logger) {
58
+ this.callbacks.logger('error', '[RecogSDK] Received primitive msg.data from server', {
59
+ dataType: typeof msg.data,
60
+ data: msg.data,
61
+ fullMessage: msg
62
+ });
63
+ }
64
+ }
65
+
66
+ const msgType = (msg.data && typeof msg.data === 'object' && 'type' in msg.data ? msg.data.type : undefined) || msg.type;
67
+ const msgData = msg.data || msg;
68
+
69
+ switch (msgType) {
70
+ case RecognitionResultTypeV1.TRANSCRIPTION:
71
+ this.handleTranscription(msgData as TranscriptionResultV1);
72
+ break;
73
+
74
+ case RecognitionResultTypeV1.FUNCTION_CALL:
75
+ this.callbacks.onFunctionCall(msgData as FunctionCallResultV1);
76
+ break;
77
+
78
+ case RecognitionResultTypeV1.METADATA:
79
+ this.callbacks.onMetadata(msgData as MetadataResultV1);
80
+ break;
81
+
82
+ case RecognitionResultTypeV1.ERROR:
83
+ this.callbacks.onError(msgData as ErrorResultV1);
84
+ break;
85
+
86
+ case RecognitionResultTypeV1.CLIENT_CONTROL_MESSAGE:
87
+ this.callbacks.onControlMessage(msgData as ClientControlMessageV1);
88
+ break;
89
+
90
+ default:
91
+ // Unknown message type - log if logger available
92
+ if (this.callbacks.logger) {
93
+ this.callbacks.logger('debug', '[RecogSDK] Unknown message type', { type: msgType });
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Handle transcript message and track performance metrics
100
+ * @param result - The transcription result from the server
101
+ */
102
+ private handleTranscription(result: TranscriptionResultV1): void {
103
+ // Track time to first transcript
104
+ if (!this.firstTranscriptTime && this.sessionStartTime) {
105
+ this.firstTranscriptTime = Date.now();
106
+ const timeToFirstTranscript = this.firstTranscriptTime - this.sessionStartTime;
107
+
108
+ if (this.callbacks.logger) {
109
+ this.callbacks.logger('debug', '[RecogSDK] First transcript received', {
110
+ timeToFirstTranscriptMs: timeToFirstTranscript
111
+ });
112
+ }
113
+ }
114
+
115
+ this.callbacks.onTranscript(result);
116
+ }
117
+
118
+ /**
119
+ * Get performance metrics
120
+ */
121
+ getMetrics() {
122
+ return {
123
+ sessionStartTime: this.sessionStartTime,
124
+ firstTranscriptTime: this.firstTranscriptTime,
125
+ timeToFirstTranscript:
126
+ this.firstTranscriptTime && this.sessionStartTime
127
+ ? this.firstTranscriptTime - this.sessionStartTime
128
+ : null
129
+ };
130
+ }
131
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Unit tests for URL Builder
3
+ */
4
+
5
+ import { RecognitionContextTypeV1, STAGES } from '@recog/shared-types';
6
+
7
+ // Mock the shared-config module BEFORE importing the module under test
8
+ const mockGetRecognitionServiceBase = jest.fn();
9
+ jest.mock('@recog/shared-config', () => ({
10
+ getRecognitionServiceBase: mockGetRecognitionServiceBase
11
+ }));
12
+
13
+ import { buildWebSocketUrl, UrlBuilderConfig } from './url-builder.js';
14
+
15
+ describe('buildWebSocketUrl', () => {
16
+ const baseConfig: UrlBuilderConfig = {
17
+ audioUtteranceId: 'test-utterance-123'
18
+ };
19
+
20
+ beforeEach(() => {
21
+ // Clear and reset mock before each test
22
+ mockGetRecognitionServiceBase.mockClear();
23
+ mockGetRecognitionServiceBase.mockReturnValue({
24
+ wsBase: 'wss://recognition.volley.com'
25
+ });
26
+ });
27
+
28
+ it('should build URL with only audioUtteranceId', () => {
29
+ const url = buildWebSocketUrl(baseConfig);
30
+ expect(url).toContain('audioUtteranceId=test-utterance-123');
31
+ });
32
+
33
+ it('should use default production URL if neither url nor stage provided', () => {
34
+ const url = buildWebSocketUrl(baseConfig);
35
+ expect(url).toContain('wss://recognition.volley.com/ws/v1/recognize');
36
+ expect(mockGetRecognitionServiceBase).toHaveBeenCalledWith('production');
37
+ });
38
+
39
+ it('should use stage parameter to build URL', () => {
40
+ mockGetRecognitionServiceBase.mockReturnValue({
41
+ wsBase: 'wss://recognition-staging.volley.com'
42
+ });
43
+ const config = {
44
+ ...baseConfig,
45
+ stage: 'staging'
46
+ };
47
+ const url = buildWebSocketUrl(config);
48
+ expect(url).toContain('wss://recognition-staging.volley.com/ws/v1/recognize');
49
+ expect(mockGetRecognitionServiceBase).toHaveBeenCalledWith('staging');
50
+ });
51
+
52
+ it('should accept Stage enum as stage parameter', () => {
53
+ mockGetRecognitionServiceBase.mockReturnValue({
54
+ wsBase: 'wss://recognition-dev.volley.com'
55
+ });
56
+ const config = {
57
+ ...baseConfig,
58
+ stage: STAGES.DEV
59
+ };
60
+ const url = buildWebSocketUrl(config);
61
+ expect(url).toContain('wss://recognition-dev.volley.com/ws/v1/recognize');
62
+ expect(mockGetRecognitionServiceBase).toHaveBeenCalledWith(STAGES.DEV);
63
+ });
64
+
65
+ it('should prioritize url over stage when both provided', () => {
66
+ const config = {
67
+ ...baseConfig,
68
+ url: 'ws://localhost:3101/ws/v1/recognize',
69
+ stage: 'staging'
70
+ };
71
+ const url = buildWebSocketUrl(config);
72
+ expect(url).toContain('ws://localhost:3101/ws/v1/recognize');
73
+ // Mock should not be called when explicit URL is provided
74
+ expect(mockGetRecognitionServiceBase).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it('should use custom URL if provided (backward compatibility)', () => {
78
+ const config = {
79
+ ...baseConfig,
80
+ url: 'ws://localhost:3101/ws/v1/recognize'
81
+ };
82
+ const url = buildWebSocketUrl(config);
83
+ expect(url).toContain('ws://localhost:3101/ws/v1/recognize');
84
+ // Mock should not be called when explicit URL is provided
85
+ expect(mockGetRecognitionServiceBase).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('should add userId to query parameters', () => {
89
+ const config = {
90
+ ...baseConfig,
91
+ userId: 'user-123'
92
+ };
93
+ const url = buildWebSocketUrl(config);
94
+ expect(url).toContain('userId=user-123');
95
+ });
96
+
97
+ it('should add gameSessionId to query parameters', () => {
98
+ const config = {
99
+ ...baseConfig,
100
+ gameSessionId: 'session-456'
101
+ };
102
+ const url = buildWebSocketUrl(config);
103
+ expect(url).toContain('gameSessionId=session-456');
104
+ });
105
+
106
+ it('should add deviceId to query parameters', () => {
107
+ const config = {
108
+ ...baseConfig,
109
+ deviceId: 'device-789'
110
+ };
111
+ const url = buildWebSocketUrl(config);
112
+ expect(url).toContain('deviceId=device-789');
113
+ });
114
+
115
+ it('should add accountId to query parameters', () => {
116
+ const config = {
117
+ ...baseConfig,
118
+ accountId: 'account-abc'
119
+ };
120
+ const url = buildWebSocketUrl(config);
121
+ expect(url).toContain('accountId=account-abc');
122
+ });
123
+
124
+ it('should add questionAnswerId to query parameters', () => {
125
+ const config = {
126
+ ...baseConfig,
127
+ questionAnswerId: 'qa-xyz'
128
+ };
129
+ const url = buildWebSocketUrl(config);
130
+ expect(url).toContain('questionAnswerId=qa-xyz');
131
+ });
132
+
133
+ it('should add platform to query parameters', () => {
134
+ const config = {
135
+ ...baseConfig,
136
+ platform: 'ios'
137
+ };
138
+ const url = buildWebSocketUrl(config);
139
+ expect(url).toContain('platform=ios');
140
+ });
141
+
142
+ it('should add gameId and gamePhase from gameContext', () => {
143
+ const config = {
144
+ ...baseConfig,
145
+ gameContext: {
146
+ type: RecognitionContextTypeV1.GAME_CONTEXT as const,
147
+ gameId: 'test-game',
148
+ gamePhase: 'test-phase'
149
+ }
150
+ };
151
+ const url = buildWebSocketUrl(config);
152
+ expect(url).toContain('gameId=test-game');
153
+ expect(url).toContain('gamePhase=test-phase');
154
+ });
155
+
156
+ it('should serialize callbackUrls as JSON', () => {
157
+ const callbackUrls = [
158
+ { url: 'https://example.com/callback', messageTypes: ['transcript'] },
159
+ { url: 'https://example.com/metadata', messageTypes: ['metadata'] }
160
+ ];
161
+ const config = {
162
+ ...baseConfig,
163
+ callbackUrls
164
+ };
165
+ const url = buildWebSocketUrl(config);
166
+ expect(url).toContain('callbackUrls=');
167
+ // Decode and verify JSON structure
168
+ const urlObj = new URL(url);
169
+ const callbackUrlsParam = urlObj.searchParams.get('callbackUrls');
170
+ expect(callbackUrlsParam).toBeDefined();
171
+ expect(JSON.parse(callbackUrlsParam!)).toEqual(callbackUrls);
172
+ });
173
+
174
+ it('should not add callbackUrls if empty array', () => {
175
+ const config = {
176
+ ...baseConfig,
177
+ callbackUrls: []
178
+ };
179
+ const url = buildWebSocketUrl(config);
180
+ expect(url).not.toContain('callbackUrls=');
181
+ });
182
+
183
+ it('should not add optional parameters if not provided', () => {
184
+ const url = buildWebSocketUrl(baseConfig);
185
+ expect(url).not.toContain('userId=');
186
+ expect(url).not.toContain('gameSessionId=');
187
+ expect(url).not.toContain('deviceId=');
188
+ expect(url).not.toContain('accountId=');
189
+ expect(url).not.toContain('questionAnswerId=');
190
+ expect(url).not.toContain('platform=');
191
+ expect(url).not.toContain('gameId=');
192
+ expect(url).not.toContain('gamePhase=');
193
+ });
194
+
195
+ it('should handle all parameters together', () => {
196
+ const config: UrlBuilderConfig = {
197
+ url: 'ws://localhost:3101/ws/v1/recognize',
198
+ audioUtteranceId: 'test-utterance-123',
199
+ userId: 'user-123',
200
+ gameSessionId: 'session-456',
201
+ deviceId: 'device-789',
202
+ accountId: 'account-abc',
203
+ questionAnswerId: 'qa-xyz',
204
+ platform: 'ios',
205
+ gameContext: {
206
+ type: RecognitionContextTypeV1.GAME_CONTEXT as const,
207
+ gameId: 'test-game',
208
+ gamePhase: 'test-phase'
209
+ },
210
+ callbackUrls: [
211
+ { url: 'https://example.com/callback', messageTypes: ['transcript'] }
212
+ ]
213
+ };
214
+
215
+ const url = buildWebSocketUrl(config);
216
+ expect(url).toContain('ws://localhost:3101/ws/v1/recognize');
217
+ expect(url).toContain('audioUtteranceId=test-utterance-123');
218
+ expect(url).toContain('userId=user-123');
219
+ expect(url).toContain('gameSessionId=session-456');
220
+ expect(url).toContain('deviceId=device-789');
221
+ expect(url).toContain('accountId=account-abc');
222
+ expect(url).toContain('questionAnswerId=qa-xyz');
223
+ expect(url).toContain('platform=ios');
224
+ expect(url).toContain('gameId=test-game');
225
+ expect(url).toContain('gamePhase=test-phase');
226
+ expect(url).toContain('callbackUrls=');
227
+ });
228
+
229
+ it('should properly encode special characters in parameters', () => {
230
+ const config = {
231
+ ...baseConfig,
232
+ userId: 'user@example.com',
233
+ platform: 'iOS 17.0'
234
+ };
235
+ const url = buildWebSocketUrl(config);
236
+ // URL should be properly encoded
237
+ expect(url).toBeDefined();
238
+ const urlObj = new URL(url);
239
+ expect(urlObj.searchParams.get('userId')).toBe('user@example.com');
240
+ expect(urlObj.searchParams.get('platform')).toBe('iOS 17.0');
241
+ });
242
+
243
+ it('should create valid URL object', () => {
244
+ const config = {
245
+ ...baseConfig,
246
+ userId: 'user-123'
247
+ };
248
+ const url = buildWebSocketUrl(config);
249
+ // Should not throw
250
+ expect(() => new URL(url)).not.toThrow();
251
+ });
252
+ });