@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.
- package/README.md +344 -0
- package/dist/browser.bundled.d.ts +1280 -0
- package/dist/browser.d.ts +10 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/config-builder.d.ts +134 -0
- package/dist/config-builder.d.ts.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/factory.d.ts +36 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/index.bundled.d.ts +2572 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10199 -0
- package/dist/index.js.map +7 -0
- package/dist/recog-client-sdk.browser.d.ts +10 -0
- package/dist/recog-client-sdk.browser.d.ts.map +1 -0
- package/dist/recog-client-sdk.browser.js +5746 -0
- package/dist/recog-client-sdk.browser.js.map +7 -0
- package/dist/recognition-client.d.ts +128 -0
- package/dist/recognition-client.d.ts.map +1 -0
- package/dist/recognition-client.types.d.ts +271 -0
- package/dist/recognition-client.types.d.ts.map +1 -0
- package/dist/simplified-vgf-recognition-client.d.ts +178 -0
- package/dist/simplified-vgf-recognition-client.d.ts.map +1 -0
- package/dist/utils/audio-ring-buffer.d.ts +69 -0
- package/dist/utils/audio-ring-buffer.d.ts.map +1 -0
- package/dist/utils/message-handler.d.ts +45 -0
- package/dist/utils/message-handler.d.ts.map +1 -0
- package/dist/utils/url-builder.d.ts +28 -0
- package/dist/utils/url-builder.d.ts.map +1 -0
- package/dist/vgf-recognition-mapper.d.ts +66 -0
- package/dist/vgf-recognition-mapper.d.ts.map +1 -0
- package/dist/vgf-recognition-state.d.ts +91 -0
- package/dist/vgf-recognition-state.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/browser.ts +24 -0
- package/src/config-builder.spec.ts +265 -0
- package/src/config-builder.ts +240 -0
- package/src/errors.ts +84 -0
- package/src/factory.spec.ts +215 -0
- package/src/factory.ts +47 -0
- package/src/index.ts +127 -0
- package/src/recognition-client.spec.ts +889 -0
- package/src/recognition-client.ts +844 -0
- package/src/recognition-client.types.ts +338 -0
- package/src/simplified-vgf-recognition-client.integration.spec.ts +718 -0
- package/src/simplified-vgf-recognition-client.spec.ts +1525 -0
- package/src/simplified-vgf-recognition-client.ts +524 -0
- package/src/utils/audio-ring-buffer.spec.ts +335 -0
- package/src/utils/audio-ring-buffer.ts +170 -0
- package/src/utils/message-handler.spec.ts +311 -0
- package/src/utils/message-handler.ts +131 -0
- package/src/utils/url-builder.spec.ts +252 -0
- package/src/utils/url-builder.ts +92 -0
- package/src/vgf-recognition-mapper.spec.ts +78 -0
- package/src/vgf-recognition-mapper.ts +232 -0
- 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
|
+
});
|