@webex/internal-plugin-voicea 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.2
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/dist/constants.js +9 -1
- package/dist/constants.js.map +1 -1
- package/dist/types/constants.d.ts +1 -0
- package/dist/types/voicea.d.ts +56 -2
- package/dist/voicea.js +173 -24
- package/dist/voicea.js.map +1 -1
- package/package.json +9 -9
- package/src/constants.ts +2 -0
- package/src/voicea.ts +163 -24
- package/test/unit/spec/voicea.js +498 -20
package/test/unit/spec/voicea.js
CHANGED
|
@@ -7,7 +7,11 @@ import Mercury from '@webex/internal-plugin-mercury';
|
|
|
7
7
|
import LLMChannel from '@webex/internal-plugin-llm';
|
|
8
8
|
|
|
9
9
|
import VoiceaService from '../../../src/index';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
EVENT_TRIGGERS,
|
|
12
|
+
LLM_PRACTICE_SESSION,
|
|
13
|
+
TOGGLE_MANUAL_CAPTION_STATUS,
|
|
14
|
+
} from '../../../src/constants';
|
|
11
15
|
|
|
12
16
|
describe('plugin-voicea', () => {
|
|
13
17
|
const locusUrl = 'locusUrl';
|
|
@@ -28,6 +32,7 @@ describe('plugin-voicea', () => {
|
|
|
28
32
|
voiceaService.connect = sinon.stub().resolves(true);
|
|
29
33
|
voiceaService.webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
30
34
|
voiceaService.webex.internal.llm.getBinding = sinon.stub().returns(undefined);
|
|
35
|
+
voiceaService.webex.internal.llm.getSocket = sinon.stub().returns(undefined);
|
|
31
36
|
voiceaService.webex.internal.llm.getLocusUrl = sinon.stub().returns(locusUrl);
|
|
32
37
|
|
|
33
38
|
voiceaService.request = sinon.stub().resolves({
|
|
@@ -87,7 +92,34 @@ describe('plugin-voicea', () => {
|
|
|
87
92
|
|
|
88
93
|
voiceaService.sendAnnouncement();
|
|
89
94
|
|
|
90
|
-
assert.
|
|
95
|
+
assert.calledTwice(spy);
|
|
96
|
+
assert.calledWith(spy, 'event:relay.event', sinon.match.func);
|
|
97
|
+
assert.calledWith(spy, `event:relay.event:${LLM_PRACTICE_SESSION}`, sinon.match.func);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('includes captionServiceId in headers when set', () => {
|
|
101
|
+
const mockWebSocket = new MockWebSocket();
|
|
102
|
+
|
|
103
|
+
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
104
|
+
voiceaService.announceStatus = 'idle';
|
|
105
|
+
voiceaService.captionServiceId = 'svc-123';
|
|
106
|
+
|
|
107
|
+
voiceaService.sendAnnouncement();
|
|
108
|
+
|
|
109
|
+
assert.calledOnceWithExactly(voiceaService.webex.internal.llm.socket.send, {
|
|
110
|
+
id: '1',
|
|
111
|
+
type: 'publishRequest',
|
|
112
|
+
recipients: {route: undefined},
|
|
113
|
+
headers: {to: 'svc-123'},
|
|
114
|
+
data: {
|
|
115
|
+
clientPayload: {
|
|
116
|
+
version: 'v2',
|
|
117
|
+
},
|
|
118
|
+
eventType: 'relay.event',
|
|
119
|
+
relayType: 'client.annc',
|
|
120
|
+
},
|
|
121
|
+
trackingId: sinon.match.string,
|
|
122
|
+
});
|
|
91
123
|
});
|
|
92
124
|
});
|
|
93
125
|
|
|
@@ -189,32 +221,32 @@ describe('plugin-voicea', () => {
|
|
|
189
221
|
assert.notCalled(voiceaService.webex.internal.llm.socket.send);
|
|
190
222
|
});
|
|
191
223
|
});
|
|
192
|
-
|
|
193
224
|
describe('#deregisterEvents', () => {
|
|
194
225
|
beforeEach(async () => {
|
|
195
226
|
const mockWebSocket = new MockWebSocket();
|
|
196
|
-
|
|
197
227
|
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
228
|
+
voiceaService.isCaptionBoxOn = true;
|
|
198
229
|
});
|
|
199
230
|
|
|
200
|
-
it('deregisters voicea service', async () => {
|
|
231
|
+
it('deregisters voicea service and resets caption state', async () => {
|
|
201
232
|
voiceaService.listenToEvents();
|
|
202
233
|
await voiceaService.toggleTranscribing(true);
|
|
203
234
|
|
|
204
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
205
235
|
voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
206
236
|
headers: {from: 'ws'},
|
|
207
237
|
data: {relayType: 'voicea.annc', voiceaPayload: {}},
|
|
208
238
|
});
|
|
209
239
|
|
|
210
240
|
assert.equal(voiceaService.areCaptionsEnabled, true);
|
|
211
|
-
assert.equal(voiceaService.
|
|
241
|
+
assert.equal(voiceaService.captionServiceId, 'ws');
|
|
242
|
+
assert.equal(voiceaService.isCaptionBoxOn, true);
|
|
212
243
|
|
|
213
244
|
voiceaService.deregisterEvents();
|
|
214
245
|
assert.equal(voiceaService.areCaptionsEnabled, false);
|
|
215
|
-
assert.equal(voiceaService.
|
|
246
|
+
assert.equal(voiceaService.captionServiceId, undefined);
|
|
216
247
|
assert.equal(voiceaService.announceStatus, 'idle');
|
|
217
248
|
assert.equal(voiceaService.captionStatus, 'idle');
|
|
249
|
+
assert.equal(voiceaService.isCaptionBoxOn, false);
|
|
218
250
|
});
|
|
219
251
|
});
|
|
220
252
|
describe('#processAnnouncementMessage', () => {
|
|
@@ -244,7 +276,7 @@ describe('plugin-voicea', () => {
|
|
|
244
276
|
});
|
|
245
277
|
});
|
|
246
278
|
|
|
247
|
-
it('works on
|
|
279
|
+
it('works on empty payload', async () => {
|
|
248
280
|
const spy = sinon.spy();
|
|
249
281
|
|
|
250
282
|
voiceaService.on(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, spy);
|
|
@@ -286,6 +318,28 @@ describe('plugin-voicea', () => {
|
|
|
286
318
|
trackingId: sinon.match.string,
|
|
287
319
|
});
|
|
288
320
|
});
|
|
321
|
+
|
|
322
|
+
it('uses captionServiceId as "to" header when set', () => {
|
|
323
|
+
voiceaService.captionServiceId = 'svc-456';
|
|
324
|
+
|
|
325
|
+
voiceaService.requestLanguage('fr');
|
|
326
|
+
|
|
327
|
+
assert.calledOnceWithExactly(voiceaService.webex.internal.llm.socket.send, {
|
|
328
|
+
id: '1',
|
|
329
|
+
type: 'publishRequest',
|
|
330
|
+
recipients: {route: undefined},
|
|
331
|
+
headers: {to: 'svc-456'},
|
|
332
|
+
data: {
|
|
333
|
+
clientPayload: {
|
|
334
|
+
translationLanguage: 'fr',
|
|
335
|
+
id: sinon.match.string,
|
|
336
|
+
},
|
|
337
|
+
eventType: 'relay.event',
|
|
338
|
+
relayType: 'voicea.transl.req',
|
|
339
|
+
},
|
|
340
|
+
trackingId: sinon.match.string,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
289
343
|
});
|
|
290
344
|
|
|
291
345
|
describe('#setSpokenLanguage', () => {
|
|
@@ -354,6 +408,7 @@ describe('plugin-voicea', () => {
|
|
|
354
408
|
|
|
355
409
|
it('turns on captions', async () => {
|
|
356
410
|
const announcementSpy = sinon.spy(voiceaService, 'announce');
|
|
411
|
+
const updateSubchannelSubscriptionsAndSyncCaptionStateSpy = sinon.spy(voiceaService, 'updateSubchannelSubscriptionsAndSyncCaptionState');
|
|
357
412
|
|
|
358
413
|
const triggerSpy = sinon.spy();
|
|
359
414
|
|
|
@@ -374,6 +429,11 @@ describe('plugin-voicea', () => {
|
|
|
374
429
|
assert.calledOnceWithExactly(triggerSpy);
|
|
375
430
|
|
|
376
431
|
assert.calledOnce(announcementSpy);
|
|
432
|
+
assert.calledOnceWithExactly(
|
|
433
|
+
updateSubchannelSubscriptionsAndSyncCaptionStateSpy,
|
|
434
|
+
{ subscribe: ['transcription'] },
|
|
435
|
+
true
|
|
436
|
+
);
|
|
377
437
|
});
|
|
378
438
|
|
|
379
439
|
it("should handle request fail", async () => {
|
|
@@ -408,17 +468,61 @@ describe('plugin-voicea', () => {
|
|
|
408
468
|
});
|
|
409
469
|
});
|
|
410
470
|
|
|
471
|
+
describe('#isLLMConnected', () => {
|
|
472
|
+
it('returns true when the default llm connection is connected', () => {
|
|
473
|
+
voiceaService.webex.internal.llm.isConnected.callsFake((channel) =>
|
|
474
|
+
channel === LLM_PRACTICE_SESSION ? false : true
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
assert.equal(voiceaService.isLLMConnected(), true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('returns true when only the practice session llm connection is connected', () => {
|
|
481
|
+
voiceaService.webex.internal.llm.isConnected.callsFake((channel) =>
|
|
482
|
+
channel === LLM_PRACTICE_SESSION
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
assert.equal(voiceaService.isLLMConnected(), true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('returns false when neither llm connection is connected', () => {
|
|
489
|
+
voiceaService.webex.internal.llm.isConnected.returns(false);
|
|
490
|
+
|
|
491
|
+
assert.equal(voiceaService.isLLMConnected(), false);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('#getIsCaptionBoxOn', () => {
|
|
496
|
+
beforeEach(() => {
|
|
497
|
+
voiceaService.isCaptionBoxOn = false;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('returns false when captions are disabled', () => {
|
|
501
|
+
voiceaService.isCaptionBoxOn = false;
|
|
502
|
+
|
|
503
|
+
const result = voiceaService.getIsCaptionBoxOn();
|
|
504
|
+
|
|
505
|
+
assert.equal(result, false);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('returns true when captions are enabled', () => {
|
|
509
|
+
voiceaService.isCaptionBoxOn = true;
|
|
510
|
+
|
|
511
|
+
const result = voiceaService.getIsCaptionBoxOn();
|
|
512
|
+
|
|
513
|
+
assert.equal(result, true);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
411
517
|
describe("#announce", () => {
|
|
412
|
-
let
|
|
518
|
+
let isAnnounceProcessed, sendAnnouncement;
|
|
413
519
|
beforeEach(() => {
|
|
414
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
415
520
|
sendAnnouncement = sinon.stub(voiceaService, 'sendAnnouncement');
|
|
416
|
-
|
|
521
|
+
isAnnounceProcessed = sinon.stub(voiceaService, 'isAnnounceProcessed').returns(false)
|
|
417
522
|
});
|
|
418
523
|
|
|
419
524
|
afterEach(() => {
|
|
420
|
-
|
|
421
|
-
isAnnounceProcessing.restore();
|
|
525
|
+
isAnnounceProcessed.restore();
|
|
422
526
|
sendAnnouncement.restore();
|
|
423
527
|
});
|
|
424
528
|
|
|
@@ -433,8 +537,18 @@ describe('plugin-voicea', () => {
|
|
|
433
537
|
assert.notCalled(sendAnnouncement);
|
|
434
538
|
});
|
|
435
539
|
|
|
540
|
+
it('announce to llm data channel when only practice session is connected', ()=> {
|
|
541
|
+
voiceaService.webex.internal.llm.isConnected.callsFake((channel) =>
|
|
542
|
+
channel === LLM_PRACTICE_SESSION
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
voiceaService.announce();
|
|
546
|
+
|
|
547
|
+
assert.calledOnce(sendAnnouncement);
|
|
548
|
+
});
|
|
549
|
+
|
|
436
550
|
it('should not announce duplicate', () => {
|
|
437
|
-
|
|
551
|
+
isAnnounceProcessed.returns(true);
|
|
438
552
|
voiceaService.announce();
|
|
439
553
|
assert.notCalled(sendAnnouncement);
|
|
440
554
|
})
|
|
@@ -463,13 +577,11 @@ describe('plugin-voicea', () => {
|
|
|
463
577
|
beforeEach(() => {
|
|
464
578
|
requestTurnOnCaptions = sinon.stub(voiceaService, 'requestTurnOnCaptions');
|
|
465
579
|
voiceaService.captionStatus = 'idle';
|
|
466
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
467
580
|
});
|
|
468
581
|
|
|
469
582
|
afterEach(() => {
|
|
470
583
|
requestTurnOnCaptions.restore();
|
|
471
584
|
voiceaService.captionStatus = 'idle';
|
|
472
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
473
585
|
});
|
|
474
586
|
|
|
475
587
|
it('call request turn on captions', () => {
|
|
@@ -478,13 +590,27 @@ describe('plugin-voicea', () => {
|
|
|
478
590
|
assert.calledOnce(requestTurnOnCaptions);
|
|
479
591
|
});
|
|
480
592
|
|
|
481
|
-
it(
|
|
593
|
+
it('throws before turning on captions when llm is not connected', async () => {
|
|
482
594
|
voiceaService.captionStatus = 'idle';
|
|
483
|
-
voiceaService.webex.internal.llm.isConnected.returns(
|
|
484
|
-
|
|
595
|
+
voiceaService.webex.internal.llm.isConnected.returns(false);
|
|
596
|
+
|
|
597
|
+
await assert.isRejected(
|
|
598
|
+
voiceaService.turnOnCaptions(),
|
|
599
|
+
'can not turn on captions before llm connected'
|
|
600
|
+
);
|
|
485
601
|
assert.notCalled(requestTurnOnCaptions);
|
|
486
602
|
});
|
|
487
603
|
|
|
604
|
+
it('turns on captions when only the practice session llm connection is connected', () => {
|
|
605
|
+
voiceaService.webex.internal.llm.isConnected.callsFake((channel) =>
|
|
606
|
+
channel === LLM_PRACTICE_SESSION
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
voiceaService.turnOnCaptions();
|
|
610
|
+
|
|
611
|
+
assert.calledOnce(requestTurnOnCaptions);
|
|
612
|
+
});
|
|
613
|
+
|
|
488
614
|
it('should not turn on duplicate when processing', () => {
|
|
489
615
|
voiceaService.captionStatus = 'sending';
|
|
490
616
|
voiceaService.turnOnCaptions();
|
|
@@ -1101,5 +1227,357 @@ describe('plugin-voicea', () => {
|
|
|
1101
1227
|
assert.calledOnceWithExactly(triggerSpy, {languageCode, meetingId: '123'});
|
|
1102
1228
|
});
|
|
1103
1229
|
});
|
|
1230
|
+
|
|
1231
|
+
describe('#onCaptionServiceIdUpdate', () => {
|
|
1232
|
+
let mockWebSocket;
|
|
1233
|
+
|
|
1234
|
+
beforeEach(() => {
|
|
1235
|
+
mockWebSocket = new MockWebSocket();
|
|
1236
|
+
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
1237
|
+
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
1238
|
+
voiceaService.seqNum = 1;
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it('does nothing when serviceId is falsy', () => {
|
|
1242
|
+
voiceaService.captionServiceId = 'existing-id';
|
|
1243
|
+
voiceaService.currentCaptionLanguage = 'en';
|
|
1244
|
+
|
|
1245
|
+
voiceaService.onCaptionServiceIdUpdate(undefined);
|
|
1246
|
+
voiceaService.onCaptionServiceIdUpdate('');
|
|
1247
|
+
|
|
1248
|
+
assert.equal(voiceaService.captionServiceId, 'existing-id');
|
|
1249
|
+
assert.notCalled(voiceaService.webex.internal.llm.socket.send);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('sets captionServiceId when no currentCaptionLanguage', () => {
|
|
1253
|
+
voiceaService.captionServiceId = undefined;
|
|
1254
|
+
voiceaService.currentCaptionLanguage = undefined;
|
|
1255
|
+
|
|
1256
|
+
voiceaService.onCaptionServiceIdUpdate('svc-new');
|
|
1257
|
+
|
|
1258
|
+
assert.equal(voiceaService.captionServiceId, 'svc-new');
|
|
1259
|
+
assert.notCalled(voiceaService.webex.internal.llm.socket.send);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it('re-sends language when serviceId changes and currentCaptionLanguage is set', () => {
|
|
1263
|
+
voiceaService.captionServiceId = 'old-svc';
|
|
1264
|
+
voiceaService.currentCaptionLanguage = 'es';
|
|
1265
|
+
|
|
1266
|
+
voiceaService.onCaptionServiceIdUpdate('new-svc');
|
|
1267
|
+
|
|
1268
|
+
assert.equal(voiceaService.captionServiceId, 'new-svc');
|
|
1269
|
+
assert.calledOnce(voiceaService.webex.internal.llm.socket.send);
|
|
1270
|
+
|
|
1271
|
+
const callArgs = voiceaService.webex.internal.llm.socket.send.getCall(0).args[0];
|
|
1272
|
+
expect(callArgs).to.have.nested.property('headers.to', 'new-svc');
|
|
1273
|
+
expect(callArgs).to.have.nested.property('data.clientPayload.translationLanguage', 'es');
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
it('does not re-send language when serviceId is unchanged', () => {
|
|
1277
|
+
voiceaService.captionServiceId = 'same-svc';
|
|
1278
|
+
voiceaService.currentCaptionLanguage = 'de';
|
|
1279
|
+
|
|
1280
|
+
voiceaService.onCaptionServiceIdUpdate('same-svc');
|
|
1281
|
+
|
|
1282
|
+
assert.equal(voiceaService.captionServiceId, 'same-svc');
|
|
1283
|
+
assert.notCalled(voiceaService.webex.internal.llm.socket.send);
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
describe('#updateSubchannelSubscriptions', () => {
|
|
1288
|
+
beforeEach(() => {
|
|
1289
|
+
const mockWebSocket = new MockWebSocket();
|
|
1290
|
+
|
|
1291
|
+
sinon.stub(voiceaService, 'getPublishTransport').returns({
|
|
1292
|
+
socket: mockWebSocket,
|
|
1293
|
+
datachannelUrl: 'mock-datachannel-uri',
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
voiceaService.seqNum = 1;
|
|
1297
|
+
|
|
1298
|
+
voiceaService.isLLMConnected = sinon.stub().returns(true);
|
|
1299
|
+
voiceaService.webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(true);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it('sends subchannelSubscriptionRequest with subscribe and unsubscribe lists', async () => {
|
|
1303
|
+
await voiceaService.updateSubchannelSubscriptions({
|
|
1304
|
+
subscribe: ['transcription'],
|
|
1305
|
+
unsubscribe: ['polls'],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const socket = voiceaService.getPublishTransport().socket;
|
|
1309
|
+
|
|
1310
|
+
sinon.assert.calledOnceWithExactly(
|
|
1311
|
+
socket.send,
|
|
1312
|
+
{
|
|
1313
|
+
id: '1',
|
|
1314
|
+
type: 'subchannelSubscriptionRequest',
|
|
1315
|
+
data: {
|
|
1316
|
+
datachannelUri: 'mock-datachannel-uri',
|
|
1317
|
+
subscribe: ['transcription'],
|
|
1318
|
+
unsubscribe: ['polls'],
|
|
1319
|
+
},
|
|
1320
|
+
trackingId: sinon.match.string,
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
sinon.assert.match(voiceaService.seqNum, 2);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
it('sends empty arrays when no subscribe/unsubscribe provided', async () => {
|
|
1328
|
+
await voiceaService.updateSubchannelSubscriptions({});
|
|
1329
|
+
|
|
1330
|
+
const socket = voiceaService.getPublishTransport().socket;
|
|
1331
|
+
|
|
1332
|
+
sinon.assert.calledOnceWithExactly(
|
|
1333
|
+
socket.send,
|
|
1334
|
+
{
|
|
1335
|
+
id: '1',
|
|
1336
|
+
type: 'subchannelSubscriptionRequest',
|
|
1337
|
+
data: {
|
|
1338
|
+
datachannelUri: 'mock-datachannel-uri',
|
|
1339
|
+
subscribe: [],
|
|
1340
|
+
unsubscribe: [],
|
|
1341
|
+
},
|
|
1342
|
+
trackingId: sinon.match.string,
|
|
1343
|
+
}
|
|
1344
|
+
);
|
|
1345
|
+
|
|
1346
|
+
sinon.assert.match(voiceaService.seqNum, 2);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it('does nothing when LLM is not connected', async () => {
|
|
1350
|
+
voiceaService.isLLMConnected = sinon.stub().returns(false);
|
|
1351
|
+
|
|
1352
|
+
await voiceaService.updateSubchannelSubscriptions({
|
|
1353
|
+
subscribe: ['transcription'],
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
const socket = voiceaService.getPublishTransport().socket;
|
|
1357
|
+
|
|
1358
|
+
sinon.assert.notCalled(socket.send);
|
|
1359
|
+
sinon.assert.match(voiceaService.seqNum, 1);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
it('does nothing when dataChannelToken is not enabled', async () => {
|
|
1363
|
+
voiceaService.webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
1364
|
+
|
|
1365
|
+
await voiceaService.updateSubchannelSubscriptions({
|
|
1366
|
+
subscribe: ['transcription'],
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
const socket = voiceaService.getPublishTransport().socket;
|
|
1370
|
+
|
|
1371
|
+
sinon.assert.notCalled(socket.send);
|
|
1372
|
+
sinon.assert.match(voiceaService.seqNum, 1);
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
describe('#updateSubchannelSubscriptionsAndSyncCaptionState', () => {
|
|
1378
|
+
beforeEach(() => {
|
|
1379
|
+
const mockWebSocket = new MockWebSocket();
|
|
1380
|
+
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
1381
|
+
|
|
1382
|
+
voiceaService.webex.internal.llm.getDatachannelUrl = sinon.stub().returns('mock-datachannel-uri');
|
|
1383
|
+
|
|
1384
|
+
voiceaService.seqNum = 1;
|
|
1385
|
+
|
|
1386
|
+
voiceaService.isLLMConnected = sinon.stub().returns(true);
|
|
1387
|
+
voiceaService.webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(true);
|
|
1388
|
+
|
|
1389
|
+
sinon.spy(voiceaService, 'updateSubchannelSubscriptions');
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
afterEach(() => {
|
|
1393
|
+
sinon.restore();
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
it('updates caption intent and forwards subscribe/unsubscribe to updateSubchannelSubscriptions', async () => {
|
|
1397
|
+
await voiceaService.updateSubchannelSubscriptionsAndSyncCaptionState(
|
|
1398
|
+
{
|
|
1399
|
+
subscribe: ['transcription'],
|
|
1400
|
+
unsubscribe: ['polls'],
|
|
1401
|
+
},
|
|
1402
|
+
true
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
assert.equal(voiceaService.isCaptionBoxOn, true);
|
|
1406
|
+
|
|
1407
|
+
assert.calledOnceWithExactly(
|
|
1408
|
+
voiceaService.updateSubchannelSubscriptions,
|
|
1409
|
+
{
|
|
1410
|
+
subscribe: ['transcription'],
|
|
1411
|
+
unsubscribe: ['polls'],
|
|
1412
|
+
}
|
|
1413
|
+
);
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
it('sets caption intent to false when isCCBoxOpen is false', async () => {
|
|
1417
|
+
await voiceaService.updateSubchannelSubscriptionsAndSyncCaptionState(
|
|
1418
|
+
{ subscribe: ['transcription'] },
|
|
1419
|
+
false
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
assert.equal(voiceaService.isCaptionBoxOn, false);
|
|
1423
|
+
|
|
1424
|
+
assert.calledOnceWithExactly(
|
|
1425
|
+
voiceaService.updateSubchannelSubscriptions,
|
|
1426
|
+
{ subscribe: ['transcription'] }
|
|
1427
|
+
);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
it('defaults subscribe/unsubscribe to empty arrays when options is empty', async () => {
|
|
1431
|
+
await voiceaService.updateSubchannelSubscriptionsAndSyncCaptionState({}, true);
|
|
1432
|
+
|
|
1433
|
+
assert.equal(voiceaService.isCaptionBoxOn, true);
|
|
1434
|
+
|
|
1435
|
+
assert.calledOnceWithExactly(
|
|
1436
|
+
voiceaService.updateSubchannelSubscriptions,
|
|
1437
|
+
{}
|
|
1438
|
+
);
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
it('still updates caption intent even if updateSubchannelSubscriptions does nothing (e.g., LLM not connected)', async () => {
|
|
1442
|
+
voiceaService.isLLMConnected = sinon.stub().returns(false);
|
|
1443
|
+
|
|
1444
|
+
await voiceaService.updateSubchannelSubscriptionsAndSyncCaptionState(
|
|
1445
|
+
{ subscribe: ['transcription'] },
|
|
1446
|
+
true
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
assert.equal(voiceaService.isCaptionBoxOn, true);
|
|
1450
|
+
|
|
1451
|
+
assert.calledOnceWithExactly(
|
|
1452
|
+
voiceaService.updateSubchannelSubscriptions,
|
|
1453
|
+
{ subscribe: ['transcription'] }
|
|
1454
|
+
);
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
describe('#multiple llm connections', () => {
|
|
1459
|
+
let defaultSocket;
|
|
1460
|
+
let practiceSocket;
|
|
1461
|
+
let isPracticeSessionConnected;
|
|
1462
|
+
|
|
1463
|
+
beforeEach(() => {
|
|
1464
|
+
defaultSocket = new MockWebSocket();
|
|
1465
|
+
practiceSocket = new MockWebSocket();
|
|
1466
|
+
isPracticeSessionConnected = true;
|
|
1467
|
+
|
|
1468
|
+
voiceaService.webex.internal.llm.socket = defaultSocket;
|
|
1469
|
+
voiceaService.webex.internal.llm.isConnected.callsFake((channel) =>
|
|
1470
|
+
channel === LLM_PRACTICE_SESSION ? isPracticeSessionConnected : true
|
|
1471
|
+
);
|
|
1472
|
+
voiceaService.webex.internal.llm.getSocket.callsFake((channel) =>
|
|
1473
|
+
channel === LLM_PRACTICE_SESSION ? practiceSocket : undefined
|
|
1474
|
+
);
|
|
1475
|
+
voiceaService.webex.internal.llm.getBinding.callsFake((channel) =>
|
|
1476
|
+
channel === LLM_PRACTICE_SESSION ? 'practice-binding' : 'default-binding'
|
|
1477
|
+
);
|
|
1478
|
+
voiceaService.seqNum = 1;
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
it('sendAnnouncement uses the practice session socket and binding when available', () => {
|
|
1482
|
+
voiceaService.announceStatus = 'idle';
|
|
1483
|
+
|
|
1484
|
+
voiceaService.sendAnnouncement();
|
|
1485
|
+
|
|
1486
|
+
assert.calledOnce(practiceSocket.send);
|
|
1487
|
+
assert.notCalled(defaultSocket.send);
|
|
1488
|
+
|
|
1489
|
+
const sent = practiceSocket.send.getCall(0).args[0];
|
|
1490
|
+
expect(sent).to.have.nested.property('recipients.route', 'practice-binding');
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
it('sendAnnouncement falls back to the default socket and binding when the practice session is not connected', () => {
|
|
1494
|
+
voiceaService.announceStatus = 'idle';
|
|
1495
|
+
isPracticeSessionConnected = false;
|
|
1496
|
+
|
|
1497
|
+
voiceaService.sendAnnouncement();
|
|
1498
|
+
|
|
1499
|
+
assert.calledOnce(defaultSocket.send);
|
|
1500
|
+
assert.notCalled(practiceSocket.send);
|
|
1501
|
+
|
|
1502
|
+
const sent = defaultSocket.send.getCall(0).args[0];
|
|
1503
|
+
expect(sent).to.have.nested.property('recipients.route', 'default-binding');
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
it('requestLanguage uses the practice session socket and binding when available', () => {
|
|
1507
|
+
voiceaService.requestLanguage('fr');
|
|
1508
|
+
|
|
1509
|
+
assert.calledOnce(practiceSocket.send);
|
|
1510
|
+
assert.notCalled(defaultSocket.send);
|
|
1511
|
+
|
|
1512
|
+
const sent = practiceSocket.send.getCall(0).args[0];
|
|
1513
|
+
expect(sent).to.have.nested.property('recipients.route', 'practice-binding');
|
|
1514
|
+
expect(sent).to.have.nested.property('data.clientPayload.translationLanguage', 'fr');
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it('requestLanguage falls back to the default socket and binding when the practice session is not connected', () => {
|
|
1518
|
+
isPracticeSessionConnected = false;
|
|
1519
|
+
|
|
1520
|
+
voiceaService.requestLanguage('fr');
|
|
1521
|
+
|
|
1522
|
+
assert.calledOnce(defaultSocket.send);
|
|
1523
|
+
assert.notCalled(practiceSocket.send);
|
|
1524
|
+
|
|
1525
|
+
const sent = defaultSocket.send.getCall(0).args[0];
|
|
1526
|
+
expect(sent).to.have.nested.property('recipients.route', 'default-binding');
|
|
1527
|
+
expect(sent).to.have.nested.property('data.clientPayload.translationLanguage', 'fr');
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
it('sendManualClosedCaption uses the practice session socket and binding when available', () => {
|
|
1531
|
+
voiceaService.sendManualClosedCaption('caption', 123, [456], true);
|
|
1532
|
+
|
|
1533
|
+
assert.calledOnce(practiceSocket.send);
|
|
1534
|
+
assert.notCalled(defaultSocket.send);
|
|
1535
|
+
|
|
1536
|
+
const sent = practiceSocket.send.getCall(0).args[0];
|
|
1537
|
+
expect(sent).to.have.nested.property('recipients.route', 'practice-binding');
|
|
1538
|
+
expect(sent).to.have.nested.property(
|
|
1539
|
+
'data.transcriptPayload.type',
|
|
1540
|
+
'manual_caption_final_result'
|
|
1541
|
+
);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it('sendManualClosedCaption falls back to the default socket and binding when the practice session is not connected', () => {
|
|
1545
|
+
isPracticeSessionConnected = false;
|
|
1546
|
+
|
|
1547
|
+
voiceaService.sendManualClosedCaption('caption', 123, [456], false);
|
|
1548
|
+
|
|
1549
|
+
assert.calledOnce(defaultSocket.send);
|
|
1550
|
+
assert.notCalled(practiceSocket.send);
|
|
1551
|
+
|
|
1552
|
+
const sent = defaultSocket.send.getCall(0).args[0];
|
|
1553
|
+
expect(sent).to.have.nested.property('recipients.route', 'default-binding');
|
|
1554
|
+
expect(sent).to.have.nested.property(
|
|
1555
|
+
'data.transcriptPayload.type',
|
|
1556
|
+
'manual_caption_interim_result'
|
|
1557
|
+
);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
it('processes relay events from the practice session channel', async () => {
|
|
1561
|
+
const announcementSpy = sinon.spy(voiceaService, 'processAnnouncementMessage');
|
|
1562
|
+
|
|
1563
|
+
voiceaService.listenToEvents();
|
|
1564
|
+
|
|
1565
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
1566
|
+
await voiceaService.webex.internal.llm._emit(`event:relay.event:${LLM_PRACTICE_SESSION}`, {
|
|
1567
|
+
headers: {from: 'svc-practice'},
|
|
1568
|
+
data: {
|
|
1569
|
+
relayType: 'voicea.annc',
|
|
1570
|
+
voiceaPayload: {
|
|
1571
|
+
translation: {allowed_languages: ['en'], max_languages: 1},
|
|
1572
|
+
ASR: {spoken_languages: ['en']},
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
sequenceNumber: 10,
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
assert.calledOnce(announcementSpy);
|
|
1579
|
+
assert.equal(voiceaService.captionServiceId, 'svc-practice');
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1104
1582
|
});
|
|
1105
1583
|
});
|