@webex/plugin-meetings 3.12.0-next.44 → 3.12.0-next.46
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/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/controls-options-manager/index.js +17 -5
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +94 -20
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +15 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/meeting/index.d.ts +6 -0
- package/dist/types/meeting/util.d.ts +7 -0
- package/dist/webinar/index.js +68 -17
- package/dist/webinar/index.js.map +1 -1
- package/package.json +3 -3
- package/src/controls-options-manager/index.ts +22 -6
- package/src/interpretation/index.ts +25 -8
- package/src/meeting/index.ts +79 -6
- package/src/meeting/util.ts +16 -2
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +88 -21
- package/test/unit/spec/controls-options-manager/index.js +35 -32
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/meeting/index.js +183 -0
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +25 -0
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +81 -16
|
@@ -9,6 +9,7 @@ describe('plugin-meetings', () => {
|
|
|
9
9
|
describe('SimultaneousInterpretation', () => {
|
|
10
10
|
let webex;
|
|
11
11
|
let interpretation;
|
|
12
|
+
let mockMeeting;
|
|
12
13
|
|
|
13
14
|
beforeEach(() => {
|
|
14
15
|
// @ts-ignore
|
|
@@ -17,8 +18,17 @@ describe('plugin-meetings', () => {
|
|
|
17
18
|
interpretation = new SimultaneousInterpretation({}, {parent: webex});
|
|
18
19
|
interpretation.locusUrl = 'locusUrl';
|
|
19
20
|
webex.request = sinon.stub().returns(Promise.resolve('REQUEST_RETURN_VALUE'));
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
mockMeeting = {
|
|
22
|
+
locusInfo: {
|
|
23
|
+
handleLocusAPIResponse: sinon.stub(),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
webex.meetings = {
|
|
27
|
+
getMeetingByType: sinon.stub(),
|
|
28
|
+
meetingCollection: {
|
|
29
|
+
getByKey: sinon.stub().returns(mockMeeting),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
22
32
|
});
|
|
23
33
|
|
|
24
34
|
describe('#initialize', () => {
|
|
@@ -316,7 +326,8 @@ describe('plugin-meetings', () => {
|
|
|
316
326
|
order : 0,
|
|
317
327
|
isActive : true
|
|
318
328
|
},];
|
|
319
|
-
|
|
329
|
+
const mockResponse = {body: {locus: {url: 'locusUrl'}}};
|
|
330
|
+
webex.request.returns(Promise.resolve(mockResponse));
|
|
320
331
|
|
|
321
332
|
await interpretation.updateInterpreters(sampleData);
|
|
322
333
|
assert.calledOnceWithExactly(webex.request, {
|
|
@@ -328,6 +339,11 @@ describe('plugin-meetings', () => {
|
|
|
328
339
|
},
|
|
329
340
|
},
|
|
330
341
|
});
|
|
342
|
+
assert.calledOnceWithExactly(
|
|
343
|
+
mockMeeting.locusInfo.handleLocusAPIResponse,
|
|
344
|
+
mockMeeting,
|
|
345
|
+
mockResponse.body
|
|
346
|
+
);
|
|
331
347
|
});
|
|
332
348
|
|
|
333
349
|
it('rejects with error', async () => {
|
|
@@ -354,7 +370,8 @@ describe('plugin-meetings', () => {
|
|
|
354
370
|
order: 0,
|
|
355
371
|
selfParticipantId: '123',
|
|
356
372
|
});
|
|
357
|
-
|
|
373
|
+
const mockResponse = {body: {locus: {url: 'locusUrl'}}};
|
|
374
|
+
webex.request.returns(Promise.resolve(mockResponse));
|
|
358
375
|
|
|
359
376
|
await interpretation.changeDirection();
|
|
360
377
|
assert.calledOnceWithExactly(webex.request, {
|
|
@@ -369,6 +386,11 @@ describe('plugin-meetings', () => {
|
|
|
369
386
|
},
|
|
370
387
|
},
|
|
371
388
|
});
|
|
389
|
+
assert.calledOnceWithExactly(
|
|
390
|
+
mockMeeting.locusInfo.handleLocusAPIResponse,
|
|
391
|
+
mockMeeting,
|
|
392
|
+
mockResponse.body
|
|
393
|
+
);
|
|
372
394
|
});
|
|
373
395
|
|
|
374
396
|
it('request rejects with error', async () => {
|
|
@@ -13703,6 +13703,131 @@ describe('plugin-meetings', () => {
|
|
|
13703
13703
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
13704
13704
|
});
|
|
13705
13705
|
|
|
13706
|
+
describe('ownership tag', () => {
|
|
13707
|
+
beforeEach(() => {
|
|
13708
|
+
// Make the owner stub dynamic so setOwnerMeetingId() writes
|
|
13709
|
+
// propagate back to getOwnerMeetingId() reads. This mirrors the
|
|
13710
|
+
// real LLM singleton behavior so the finally-block release in
|
|
13711
|
+
// cleanupLLMConneciton is reflected in subsequent reads.
|
|
13712
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub().returns(undefined);
|
|
13713
|
+
webex.internal.llm.setOwnerMeetingId = sinon.stub().callsFake((id) => {
|
|
13714
|
+
webex.internal.llm.getOwnerMeetingId.returns(id);
|
|
13715
|
+
});
|
|
13716
|
+
});
|
|
13717
|
+
|
|
13718
|
+
it('skips disconnect and reconnect when LLM is connected and owned by another meeting (regardless of URL)', async () => {
|
|
13719
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13720
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13721
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13722
|
+
// Locus/datachannel URL mismatch is the *normal* case when
|
|
13723
|
+
// another meeting owns the live socket -- each meeting has its
|
|
13724
|
+
// own locus URL. URL mismatch must NOT trigger a reclaim,
|
|
13725
|
+
// because doing so would tear down the owning meeting's healthy
|
|
13726
|
+
// LLM socket and break its data channel.
|
|
13727
|
+
webex.internal.llm.getLocusUrl.returns('owner-locus-url');
|
|
13728
|
+
webex.internal.llm.getDatachannelUrl.returns('owner-dc-url');
|
|
13729
|
+
meeting.locusInfo = {
|
|
13730
|
+
url: 'a different url',
|
|
13731
|
+
info: {datachannelUrl: 'a different datachannel url'},
|
|
13732
|
+
self: {},
|
|
13733
|
+
};
|
|
13734
|
+
|
|
13735
|
+
const result = await meeting.updateLLMConnection();
|
|
13736
|
+
|
|
13737
|
+
assert.equal(result, undefined);
|
|
13738
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13739
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
13740
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
13741
|
+
assert.notCalled(meeting.startLLMHealthCheckTimer);
|
|
13742
|
+
});
|
|
13743
|
+
|
|
13744
|
+
|
|
13745
|
+
it('clears stale owner tag in cleanup finally block even when disconnectLLM rejects', async () => {
|
|
13746
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13747
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13748
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13749
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13750
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13751
|
+
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
13752
|
+
meeting.locusInfo = {
|
|
13753
|
+
url: 'a different url',
|
|
13754
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13755
|
+
self: {},
|
|
13756
|
+
};
|
|
13757
|
+
|
|
13758
|
+
try {
|
|
13759
|
+
await meeting.updateLLMConnection();
|
|
13760
|
+
} catch (e) {
|
|
13761
|
+
/* updateLLMConnection may reject when cleanup throws */
|
|
13762
|
+
}
|
|
13763
|
+
|
|
13764
|
+
// The owner-eligible finally branch must release the tag so a
|
|
13765
|
+
// subsequent reconnect attempt from any meeting is not blocked.
|
|
13766
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId, undefined);
|
|
13767
|
+
});
|
|
13768
|
+
|
|
13769
|
+
it('proceeds normally when LLM is connected and owned by this meeting with URL change', async () => {
|
|
13770
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13771
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13772
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13773
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13774
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13775
|
+
meeting.locusInfo = {
|
|
13776
|
+
url: 'a different url',
|
|
13777
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13778
|
+
self: {},
|
|
13779
|
+
};
|
|
13780
|
+
|
|
13781
|
+
await meeting.updateLLMConnection();
|
|
13782
|
+
|
|
13783
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13784
|
+
code: 3050,
|
|
13785
|
+
reason: 'done (permanent)',
|
|
13786
|
+
});
|
|
13787
|
+
assert.calledWithExactly(
|
|
13788
|
+
webex.internal.llm.registerAndConnect,
|
|
13789
|
+
'a different url',
|
|
13790
|
+
'a datachannel url',
|
|
13791
|
+
undefined
|
|
13792
|
+
);
|
|
13793
|
+
// setOwnerMeetingId is called twice: first with undefined in
|
|
13794
|
+
// cleanupLLMConneciton's finally block (so a failed disconnect
|
|
13795
|
+
// cannot leave a stale owner), then with this meeting's id
|
|
13796
|
+
// after registerAndConnect resolves.
|
|
13797
|
+
assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
|
|
13798
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.firstCall, undefined);
|
|
13799
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.lastCall, meeting.id);
|
|
13800
|
+
});
|
|
13801
|
+
|
|
13802
|
+
it('claims ownership after successful registerAndConnect on initial connect', async () => {
|
|
13803
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13804
|
+
webex.internal.llm.isConnected.returns(false);
|
|
13805
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
13806
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13807
|
+
|
|
13808
|
+
await meeting.updateLLMConnection();
|
|
13809
|
+
|
|
13810
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
13811
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
13812
|
+
});
|
|
13813
|
+
|
|
13814
|
+
it('proceeds to connect when LLM is not connected even if another ownerId lingers', async () => {
|
|
13815
|
+
// Defensive path: if the LLM reports not-connected but an old
|
|
13816
|
+
// ownerId is still present (e.g. race before a successful
|
|
13817
|
+
// connections.delete), this meeting can still claim a fresh
|
|
13818
|
+
// connection.
|
|
13819
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13820
|
+
webex.internal.llm.isConnected.returns(false);
|
|
13821
|
+
webex.internal.llm.getOwnerMeetingId.returns('stale-owner-id');
|
|
13822
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13823
|
+
|
|
13824
|
+
await meeting.updateLLMConnection();
|
|
13825
|
+
|
|
13826
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
13827
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
13828
|
+
});
|
|
13829
|
+
});
|
|
13830
|
+
|
|
13706
13831
|
describe('#clearMeetingData', () => {
|
|
13707
13832
|
beforeEach(() => {
|
|
13708
13833
|
webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
@@ -13763,6 +13888,64 @@ describe('plugin-meetings', () => {
|
|
|
13763
13888
|
assert.notCalled(meeting.stopTranscription);
|
|
13764
13889
|
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13765
13890
|
});
|
|
13891
|
+
|
|
13892
|
+
describe('ownership tag', () => {
|
|
13893
|
+
beforeEach(() => {
|
|
13894
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub();
|
|
13895
|
+
});
|
|
13896
|
+
|
|
13897
|
+
it('skips disconnectLLM but still removes this meeting listeners when another meeting owns the LLM', async () => {
|
|
13898
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13899
|
+
|
|
13900
|
+
await meeting.clearMeetingData();
|
|
13901
|
+
|
|
13902
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13903
|
+
// Shared data-channel auth tokens belong to the owner meeting's
|
|
13904
|
+
// live LLM session and must not be wiped by a non-owner
|
|
13905
|
+
// teardown, otherwise the owner's next reconnect would lose
|
|
13906
|
+
// its Data-Channel-Auth-Token.
|
|
13907
|
+
assert.notCalled(meeting.clearDataChannelToken);
|
|
13908
|
+
// Listeners owned by *this* Meeting instance must still be
|
|
13909
|
+
// removed so a leaving subordinate meeting stops receiving
|
|
13910
|
+
// relay/locus events from the shared singleton.
|
|
13911
|
+
assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
13912
|
+
assert.calledWithExactly(
|
|
13913
|
+
webex.internal.llm.off,
|
|
13914
|
+
'event:relay.event',
|
|
13915
|
+
meeting.processRelayEvent
|
|
13916
|
+
);
|
|
13917
|
+
assert.calledWithExactly(
|
|
13918
|
+
webex.internal.llm.off,
|
|
13919
|
+
'event:locus.state_message',
|
|
13920
|
+
meeting.processLocusLLMEvent
|
|
13921
|
+
);
|
|
13922
|
+
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13923
|
+
});
|
|
13924
|
+
|
|
13925
|
+
it('calls disconnectLLM and clears data channel token when this meeting is the owner', async () => {
|
|
13926
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13927
|
+
|
|
13928
|
+
await meeting.clearMeetingData();
|
|
13929
|
+
|
|
13930
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13931
|
+
code: 3050,
|
|
13932
|
+
reason: 'done (permanent)',
|
|
13933
|
+
});
|
|
13934
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13935
|
+
});
|
|
13936
|
+
|
|
13937
|
+
it('calls disconnectLLM and clears data channel token when no owner is recorded (first-claim / legacy)', async () => {
|
|
13938
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
13939
|
+
|
|
13940
|
+
await meeting.clearMeetingData();
|
|
13941
|
+
|
|
13942
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13943
|
+
code: 3050,
|
|
13944
|
+
reason: 'done (permanent)',
|
|
13945
|
+
});
|
|
13946
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13947
|
+
});
|
|
13948
|
+
});
|
|
13766
13949
|
});
|
|
13767
13950
|
});
|
|
13768
13951
|
|
|
@@ -11,6 +11,7 @@ describe('plugin-meetings', () => {
|
|
|
11
11
|
let audio;
|
|
12
12
|
let video;
|
|
13
13
|
let originalRemoteUpdateAudioVideo;
|
|
14
|
+
let originalUpdateLocusFromApiResponse;
|
|
14
15
|
|
|
15
16
|
const fakeLocusResponse = {body: {locus: {info: 'this is a fake locus'}}};
|
|
16
17
|
|
|
@@ -45,6 +46,7 @@ describe('plugin-meetings', () => {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
originalRemoteUpdateAudioVideo = MeetingUtil.remoteUpdateAudioVideo;
|
|
49
|
+
originalUpdateLocusFromApiResponse = MeetingUtil.updateLocusFromApiResponse;
|
|
48
50
|
|
|
49
51
|
MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocusResponse);
|
|
50
52
|
MeetingUtil.updateLocusFromApiResponse = sinon.stub();
|
|
@@ -57,6 +59,7 @@ describe('plugin-meetings', () => {
|
|
|
57
59
|
|
|
58
60
|
afterEach(() => {
|
|
59
61
|
MeetingUtil.remoteUpdateAudioVideo = originalRemoteUpdateAudioVideo;
|
|
62
|
+
MeetingUtil.updateLocusFromApiResponse = originalUpdateLocusFromApiResponse;
|
|
60
63
|
});
|
|
61
64
|
|
|
62
65
|
describe('mute state library', () => {
|
|
@@ -276,6 +276,31 @@ describe('plugin-meetings', () => {
|
|
|
276
276
|
assert.notCalled(meeting.locusInfo.handleLocusAPIResponse);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
+
it('should call handleLocusAPIResponse when response body is an unwrapped LocusDTO', () => {
|
|
280
|
+
const meeting = {
|
|
281
|
+
locusInfo: {
|
|
282
|
+
handleLocusAPIResponse: sinon.stub(),
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const originalResponse = {
|
|
287
|
+
body: {
|
|
288
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/some-id',
|
|
289
|
+
participants: [],
|
|
290
|
+
self: {},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const response = MeetingUtil.updateLocusFromApiResponse(meeting, originalResponse);
|
|
295
|
+
|
|
296
|
+
assert.deepEqual(response, originalResponse);
|
|
297
|
+
assert.calledOnceWithExactly(
|
|
298
|
+
meeting.locusInfo.handleLocusAPIResponse,
|
|
299
|
+
meeting,
|
|
300
|
+
originalResponse.body
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
279
304
|
it('should work with an undefined meeting', () => {
|
|
280
305
|
const originalResponse = {
|
|
281
306
|
body: {
|
|
@@ -35,6 +35,7 @@ describe('plugin-meetings', () => {
|
|
|
35
35
|
beforeEach(() => {
|
|
36
36
|
request = {
|
|
37
37
|
request: sinon.stub().returns(Promise.resolve()),
|
|
38
|
+
locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
controller = new RecordingController(request);
|
|
@@ -69,13 +70,13 @@ describe('plugin-meetings', () => {
|
|
|
69
70
|
|
|
70
71
|
const result = controller.startRecording();
|
|
71
72
|
|
|
72
|
-
assert.calledWith(request.
|
|
73
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
73
74
|
uri: `${locusUrl}/controls`,
|
|
74
75
|
body: {record: {recording: true, paused: false}},
|
|
75
76
|
method: HTTP_VERBS.PATCH,
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
assert.deepEqual(result, request.
|
|
79
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
@@ -103,13 +104,13 @@ describe('plugin-meetings', () => {
|
|
|
103
104
|
|
|
104
105
|
const result = controller.stopRecording();
|
|
105
106
|
|
|
106
|
-
assert.calledWith(request.
|
|
107
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
107
108
|
uri: `${locusUrl}/controls`,
|
|
108
109
|
body: {record: {recording: false, paused: false}},
|
|
109
110
|
method: HTTP_VERBS.PATCH,
|
|
110
111
|
});
|
|
111
112
|
|
|
112
|
-
assert.deepEqual(result, request.
|
|
113
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
113
114
|
});
|
|
114
115
|
});
|
|
115
116
|
|
|
@@ -139,13 +140,13 @@ describe('plugin-meetings', () => {
|
|
|
139
140
|
|
|
140
141
|
const result = controller.pauseRecording();
|
|
141
142
|
|
|
142
|
-
assert.calledWith(request.
|
|
143
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
143
144
|
uri: `${locusUrl}/controls`,
|
|
144
145
|
body: {record: {recording: true, paused: true}},
|
|
145
146
|
method: HTTP_VERBS.PATCH,
|
|
146
147
|
});
|
|
147
148
|
|
|
148
|
-
assert.deepEqual(result, request.
|
|
149
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
149
150
|
});
|
|
150
151
|
});
|
|
151
152
|
|
|
@@ -176,13 +177,13 @@ describe('plugin-meetings', () => {
|
|
|
176
177
|
|
|
177
178
|
const result = controller.resumeRecording();
|
|
178
179
|
|
|
179
|
-
assert.calledWith(request.
|
|
180
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
180
181
|
uri: `${locusUrl}/controls`,
|
|
181
182
|
body: {record: {recording: true, paused: false}},
|
|
182
183
|
method: HTTP_VERBS.PATCH,
|
|
183
184
|
});
|
|
184
185
|
|
|
185
|
-
assert.deepEqual(result, request.
|
|
186
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
186
187
|
});
|
|
187
188
|
});
|
|
188
189
|
});
|
|
@@ -176,6 +176,42 @@ describe('plugin-meetings', () => {
|
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
describe('#getValidatedWebinarMeeting', () => {
|
|
180
|
+
it('returns the meeting when its locusUrl matches the webinar locusUrl', () => {
|
|
181
|
+
const meeting = {locusUrl: 'locusUrl'};
|
|
182
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
183
|
+
webinar.locusUrl = 'locusUrl';
|
|
184
|
+
|
|
185
|
+
assert.equal(webinar.getValidatedWebinarMeeting(), meeting);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns undefined and warns when the resolved meeting locusUrl does not match', () => {
|
|
189
|
+
const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
|
|
190
|
+
const meeting = {locusUrl: 'other-locus-url'};
|
|
191
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
192
|
+
webinar.locusUrl = 'locusUrl';
|
|
193
|
+
|
|
194
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
195
|
+
assert.calledOnce(warnStub);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns undefined when no meeting is resolved', () => {
|
|
199
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(undefined);
|
|
200
|
+
|
|
201
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns undefined and warns when webinar locusUrl is not yet initialized', () => {
|
|
205
|
+
const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
|
|
206
|
+
const meeting = {locusUrl: 'some-url'};
|
|
207
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
208
|
+
webinar.locusUrl = undefined;
|
|
209
|
+
|
|
210
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
211
|
+
assert.calledOnce(warnStub);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
179
215
|
describe('#cleanUp', () => {
|
|
180
216
|
it('delegates to cleanupPSDataChannel', () => {
|
|
181
217
|
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
@@ -187,17 +223,14 @@ describe('plugin-meetings', () => {
|
|
|
187
223
|
});
|
|
188
224
|
|
|
189
225
|
describe('#cleanupPSDataChannel', () => {
|
|
190
|
-
let
|
|
226
|
+
let relayListener;
|
|
191
227
|
|
|
192
228
|
beforeEach(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
229
|
+
relayListener = sinon.stub();
|
|
230
|
+
webinar._practiceSessionRelayListener = relayListener;
|
|
198
231
|
});
|
|
199
232
|
|
|
200
|
-
it('disconnects the practice session channel and removes the relay listener', async () => {
|
|
233
|
+
it('disconnects the practice session channel and removes the tracked relay listener', async () => {
|
|
201
234
|
await webinar.cleanupPSDataChannel();
|
|
202
235
|
|
|
203
236
|
assert.calledOnceWithExactly(
|
|
@@ -208,8 +241,28 @@ describe('plugin-meetings', () => {
|
|
|
208
241
|
assert.calledOnceWithExactly(
|
|
209
242
|
webex.internal.llm.off,
|
|
210
243
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
211
|
-
|
|
244
|
+
relayListener
|
|
212
245
|
);
|
|
246
|
+
assert.isNull(webinar._practiceSessionRelayListener);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('skips relay listener removal when no listener has been tracked', async () => {
|
|
250
|
+
webinar._practiceSessionRelayListener = null;
|
|
251
|
+
|
|
252
|
+
await webinar.cleanupPSDataChannel();
|
|
253
|
+
|
|
254
|
+
const relayOffCalls = webex.internal.llm.off.args.filter(
|
|
255
|
+
([event]) => event === `event:relay.event:${LLM_PRACTICE_SESSION}`
|
|
256
|
+
);
|
|
257
|
+
assert.equal(relayOffCalls.length, 0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('does not consult the meeting collection during cleanup', async () => {
|
|
261
|
+
webex.meetings.getMeetingByType = sinon.stub();
|
|
262
|
+
|
|
263
|
+
await webinar.cleanupPSDataChannel();
|
|
264
|
+
|
|
265
|
+
assert.notCalled(webex.meetings.getMeetingByType);
|
|
213
266
|
});
|
|
214
267
|
|
|
215
268
|
it('removes a pending online listener if one exists', async () => {
|
|
@@ -240,6 +293,7 @@ describe('plugin-meetings', () => {
|
|
|
240
293
|
beforeEach(() => {
|
|
241
294
|
processRelayEvent = sinon.stub();
|
|
242
295
|
meeting = {
|
|
296
|
+
locusUrl: 'locusUrl',
|
|
243
297
|
isJoined: sinon.stub().returns(true),
|
|
244
298
|
processRelayEvent,
|
|
245
299
|
locusInfo: {
|
|
@@ -418,19 +472,30 @@ describe('plugin-meetings', () => {
|
|
|
418
472
|
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
419
473
|
});
|
|
420
474
|
|
|
421
|
-
it('
|
|
475
|
+
it('tracks and binds the relay listener after successful connect', async () => {
|
|
422
476
|
await webinar.updatePSDataChannel();
|
|
423
477
|
|
|
478
|
+
// Stores the exact listener reference for deterministic cleanup
|
|
479
|
+
assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
|
|
424
480
|
assert.calledWith(
|
|
425
|
-
webex.internal.llm.
|
|
481
|
+
webex.internal.llm.on,
|
|
426
482
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
427
483
|
processRelayEvent
|
|
428
484
|
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('removes a previously tracked relay listener before re-binding on reconnect', async () => {
|
|
488
|
+
const previousListener = sinon.stub();
|
|
489
|
+
webinar._practiceSessionRelayListener = previousListener;
|
|
490
|
+
|
|
491
|
+
await webinar.updatePSDataChannel();
|
|
492
|
+
|
|
429
493
|
assert.calledWith(
|
|
430
|
-
webex.internal.llm.
|
|
494
|
+
webex.internal.llm.off,
|
|
431
495
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
432
|
-
|
|
496
|
+
previousListener
|
|
433
497
|
);
|
|
498
|
+
assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
|
|
434
499
|
});
|
|
435
500
|
|
|
436
501
|
it('subscribes to transcription when caption intent is enabled', async () => {
|
|
@@ -551,7 +616,7 @@ describe('plugin-meetings', () => {
|
|
|
551
616
|
updateMediaShares = sinon.stub()
|
|
552
617
|
webinar.webex.meetings = {
|
|
553
618
|
getMeetingByType: sinon.stub().returns({
|
|
554
|
-
id: 'meeting-id',
|
|
619
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
555
620
|
isJoined: sinon.stub().returns(false),
|
|
556
621
|
updateLLMConnection: sinon.stub(),
|
|
557
622
|
shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
|
|
@@ -606,7 +671,7 @@ describe('plugin-meetings', () => {
|
|
|
606
671
|
|
|
607
672
|
webinar.webex.meetings = {
|
|
608
673
|
getMeetingByType: sinon.stub().returns({
|
|
609
|
-
id: 'meeting-id',
|
|
674
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
610
675
|
isJoined: sinon.stub().returns(false),
|
|
611
676
|
updateLLMConnection: sinon.stub(),
|
|
612
677
|
shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
|
|
@@ -1034,7 +1099,7 @@ describe('plugin-meetings', () => {
|
|
|
1034
1099
|
// @ts-ignore
|
|
1035
1100
|
webinar.webex.meetings = {
|
|
1036
1101
|
getMeetingByType: sinon.stub().returns({
|
|
1037
|
-
id: 'meeting-id',
|
|
1102
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
1038
1103
|
locusInfo: {
|
|
1039
1104
|
links:{
|
|
1040
1105
|
resources: {
|
|
@@ -1051,7 +1116,7 @@ describe('plugin-meetings', () => {
|
|
|
1051
1116
|
it('throws an error if attendeeSearchUrl is not available', async () => {
|
|
1052
1117
|
webinar.webex.meetings = {
|
|
1053
1118
|
getMeetingByType: sinon.stub().returns({
|
|
1054
|
-
id: 'meeting-id',
|
|
1119
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
1055
1120
|
locusInfo: {
|
|
1056
1121
|
links:{
|
|
1057
1122
|
resources: {
|