@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.51

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 (136) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +6 -2
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +1 -1
  7. package/dist/config.js +1 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/constants.js +6 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/controls-options-manager/constants.js +11 -1
  12. package/dist/controls-options-manager/constants.js.map +1 -1
  13. package/dist/controls-options-manager/index.js +38 -24
  14. package/dist/controls-options-manager/index.js.map +1 -1
  15. package/dist/controls-options-manager/util.js +91 -0
  16. package/dist/controls-options-manager/util.js.map +1 -1
  17. package/dist/hashTree/constants.js +10 -1
  18. package/dist/hashTree/constants.js.map +1 -1
  19. package/dist/hashTree/hashTreeParser.js +646 -371
  20. package/dist/hashTree/hashTreeParser.js.map +1 -1
  21. package/dist/hashTree/utils.js +22 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/locusRetry.js +23 -8
  26. package/dist/interceptors/locusRetry.js.map +1 -1
  27. package/dist/interpretation/index.js +10 -1
  28. package/dist/interpretation/index.js.map +1 -1
  29. package/dist/interpretation/siLanguage.js +1 -1
  30. package/dist/locus-info/controlsUtils.js +4 -1
  31. package/dist/locus-info/controlsUtils.js.map +1 -1
  32. package/dist/locus-info/index.js +289 -86
  33. package/dist/locus-info/index.js.map +1 -1
  34. package/dist/locus-info/types.js +19 -0
  35. package/dist/locus-info/types.js.map +1 -1
  36. package/dist/media/properties.js +1 -0
  37. package/dist/media/properties.js.map +1 -1
  38. package/dist/meeting/in-meeting-actions.js +3 -1
  39. package/dist/meeting/in-meeting-actions.js.map +1 -1
  40. package/dist/meeting/index.js +842 -521
  41. package/dist/meeting/index.js.map +1 -1
  42. package/dist/meeting/util.js +19 -2
  43. package/dist/meeting/util.js.map +1 -1
  44. package/dist/meetings/index.js +205 -77
  45. package/dist/meetings/index.js.map +1 -1
  46. package/dist/meetings/meetings.types.js +6 -1
  47. package/dist/meetings/meetings.types.js.map +1 -1
  48. package/dist/meetings/request.js +39 -0
  49. package/dist/meetings/request.js.map +1 -1
  50. package/dist/meetings/util.js +67 -5
  51. package/dist/meetings/util.js.map +1 -1
  52. package/dist/member/index.js +10 -0
  53. package/dist/member/index.js.map +1 -1
  54. package/dist/member/types.js.map +1 -1
  55. package/dist/member/util.js +3 -0
  56. package/dist/member/util.js.map +1 -1
  57. package/dist/metrics/constants.js +2 -1
  58. package/dist/metrics/constants.js.map +1 -1
  59. package/dist/recording-controller/index.js +1 -3
  60. package/dist/recording-controller/index.js.map +1 -1
  61. package/dist/types/config.d.ts +1 -0
  62. package/dist/types/constants.d.ts +2 -0
  63. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  64. package/dist/types/controls-options-manager/index.d.ts +10 -0
  65. package/dist/types/hashTree/constants.d.ts +1 -0
  66. package/dist/types/hashTree/hashTreeParser.d.ts +83 -16
  67. package/dist/types/hashTree/utils.d.ts +11 -0
  68. package/dist/types/index.d.ts +2 -0
  69. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  70. package/dist/types/locus-info/index.d.ts +46 -6
  71. package/dist/types/locus-info/types.d.ts +21 -1
  72. package/dist/types/media/properties.d.ts +1 -0
  73. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  74. package/dist/types/meeting/index.d.ts +70 -1
  75. package/dist/types/meeting/util.d.ts +8 -0
  76. package/dist/types/meetings/index.d.ts +20 -2
  77. package/dist/types/meetings/meetings.types.d.ts +15 -0
  78. package/dist/types/meetings/request.d.ts +14 -0
  79. package/dist/types/member/index.d.ts +1 -0
  80. package/dist/types/member/types.d.ts +1 -0
  81. package/dist/types/member/util.d.ts +1 -0
  82. package/dist/types/metrics/constants.d.ts +1 -0
  83. package/dist/webinar/index.js +361 -235
  84. package/dist/webinar/index.js.map +1 -1
  85. package/package.json +22 -22
  86. package/src/aiEnableRequest/index.ts +16 -0
  87. package/src/breakouts/breakout.ts +2 -1
  88. package/src/config.ts +1 -0
  89. package/src/constants.ts +5 -1
  90. package/src/controls-options-manager/constants.ts +14 -1
  91. package/src/controls-options-manager/index.ts +47 -24
  92. package/src/controls-options-manager/util.ts +81 -1
  93. package/src/hashTree/constants.ts +9 -0
  94. package/src/hashTree/hashTreeParser.ts +362 -174
  95. package/src/hashTree/utils.ts +17 -0
  96. package/src/index.ts +5 -0
  97. package/src/interceptors/locusRetry.ts +25 -4
  98. package/src/interpretation/index.ts +25 -8
  99. package/src/locus-info/controlsUtils.ts +3 -1
  100. package/src/locus-info/index.ts +291 -93
  101. package/src/locus-info/types.ts +25 -1
  102. package/src/media/properties.ts +1 -0
  103. package/src/meeting/in-meeting-actions.ts +4 -0
  104. package/src/meeting/index.ts +315 -26
  105. package/src/meeting/util.ts +20 -2
  106. package/src/meetings/index.ts +109 -43
  107. package/src/meetings/meetings.types.ts +19 -0
  108. package/src/meetings/request.ts +43 -0
  109. package/src/meetings/util.ts +80 -1
  110. package/src/member/index.ts +10 -0
  111. package/src/member/types.ts +1 -0
  112. package/src/member/util.ts +3 -0
  113. package/src/metrics/constants.ts +1 -0
  114. package/src/recording-controller/index.ts +1 -2
  115. package/src/webinar/index.ts +162 -21
  116. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  117. package/test/unit/spec/breakouts/breakout.ts +7 -3
  118. package/test/unit/spec/controls-options-manager/index.js +140 -29
  119. package/test/unit/spec/controls-options-manager/util.js +165 -0
  120. package/test/unit/spec/hashTree/hashTreeParser.ts +1341 -140
  121. package/test/unit/spec/hashTree/utils.ts +88 -1
  122. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  123. package/test/unit/spec/interpretation/index.ts +26 -4
  124. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  125. package/test/unit/spec/locus-info/index.js +475 -81
  126. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  127. package/test/unit/spec/meeting/index.js +836 -41
  128. package/test/unit/spec/meeting/muteState.js +3 -0
  129. package/test/unit/spec/meeting/utils.js +33 -0
  130. package/test/unit/spec/meetings/index.js +309 -10
  131. package/test/unit/spec/meetings/request.js +141 -0
  132. package/test/unit/spec/meetings/utils.js +161 -0
  133. package/test/unit/spec/member/index.js +7 -0
  134. package/test/unit/spec/member/util.js +24 -0
  135. package/test/unit/spec/recording-controller/index.js +9 -8
  136. package/test/unit/spec/webinar/index.ts +141 -16
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  import WebinarCollection from './collection';
20
20
  import LoggerProxy from '../common/logs/logger-proxy';
21
+ import MeetingUtil from '../meeting/util';
21
22
  import {sanitizeParams} from './utils';
22
23
 
23
24
  /**
@@ -98,13 +99,49 @@ const Webinar = WebexPlugin.extend({
98
99
  return {isPromoted, isDemoted};
99
100
  },
100
101
 
102
+ /**
103
+ * Resolves the meeting associated with this webinar instance, guarded against the
104
+ * meetingId pointer drifting onto an unrelated transient meeting (e.g. an inbound
105
+ * 1:1 call) that may exist in the meeting collection. Returns the meeting only when
106
+ * its locusUrl matches this webinar's tracked locusUrl. Returns undefined (with a
107
+ * warning) when the meeting cannot be resolved or when the webinar's locusUrl has
108
+ * not been initialized yet — callers must treat this as "no owned meeting" rather
109
+ * than fall through to an unvalidated lookup.
110
+ * @returns {object|undefined}
111
+ */
112
+ getValidatedWebinarMeeting() {
113
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
114
+
115
+ if (!meeting) {
116
+ return undefined;
117
+ }
118
+
119
+ if (!this.locusUrl) {
120
+ LoggerProxy.logger.warn(
121
+ `Webinar:index#getValidatedWebinarMeeting --> skipping; webinar locusUrl is not yet initialized for meetingId ${this.meetingId}`
122
+ );
123
+
124
+ return undefined;
125
+ }
126
+
127
+ if (meeting.locusUrl !== this.locusUrl) {
128
+ LoggerProxy.logger.warn(
129
+ `Webinar:index#getValidatedWebinarMeeting --> skipping; meeting ${this.meetingId} locusUrl ${meeting.locusUrl} does not match webinar locusUrl ${this.locusUrl}`
130
+ );
131
+
132
+ return undefined;
133
+ }
134
+
135
+ return meeting;
136
+ },
137
+
101
138
  /**
102
139
  * should join practice session data channel or not
103
140
  * @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
104
141
  * @returns {void}
105
142
  */
106
143
  updateStatusByRole({isPromoted, isDemoted}) {
107
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
144
+ const meeting = this.getValidatedWebinarMeeting();
108
145
 
109
146
  if (
110
147
  (isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
@@ -128,6 +165,9 @@ const Webinar = WebexPlugin.extend({
128
165
 
129
166
  /**
130
167
  * Disconnects the practice session data channel and removes its relay listener.
168
+ * The listener reference removed here is the exact callback captured at subscribe
169
+ * time (see updatePSDataChannel) so that cleanup is correct even if the underlying
170
+ * meeting can no longer be resolved (e.g. locusUrl mismatch).
131
171
  * @returns {Promise<void>}
132
172
  */
133
173
  async cleanupPSDataChannel() {
@@ -137,8 +177,6 @@ const Webinar = WebexPlugin.extend({
137
177
  this._pendingOnlineListener = null;
138
178
  }
139
179
 
140
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
141
-
142
180
  // @ts-ignore - Fix type
143
181
  await this.webex.internal.llm.disconnectLLM(
144
182
  {
@@ -147,11 +185,65 @@ const Webinar = WebexPlugin.extend({
147
185
  },
148
186
  LLM_PRACTICE_SESSION
149
187
  );
150
- // @ts-ignore - Fix type
151
- this.webex.internal.llm.off(
152
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
153
- meeting?.processRelayEvent
188
+
189
+ if (this._practiceSessionRelayListener) {
190
+ // @ts-ignore - Fix type
191
+ this.webex.internal.llm.off(
192
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
193
+ this._practiceSessionRelayListener
194
+ );
195
+ this._practiceSessionRelayListener = null;
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Ensures practice-session token exists before registering the practice LLM channel.
201
+ * Caller is responsible for passing a meeting that has already been resolved via
202
+ * getValidatedWebinarMeeting() — this method does not re-validate ownership.
203
+ * @param {object} meeting
204
+ * @returns {Promise<string|undefined>}
205
+ */
206
+ async ensurePracticeSessionDatachannelToken(meeting) {
207
+ // @ts-ignore
208
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
209
+
210
+ if (!isDataChannelTokenEnabled) {
211
+ return undefined;
212
+ }
213
+
214
+ // @ts-ignore
215
+ const cachedToken = this.webex.internal.llm.getDatachannelToken(
216
+ DataChannelTokenType.PracticeSession
154
217
  );
218
+
219
+ if (cachedToken) {
220
+ return cachedToken;
221
+ }
222
+
223
+ try {
224
+ const refreshResponse = await meeting.refreshDataChannelToken();
225
+ const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};
226
+
227
+ if (!datachannelToken) {
228
+ return undefined;
229
+ }
230
+
231
+ // @ts-ignore
232
+ this.webex.internal.llm.setDatachannelToken(
233
+ datachannelToken,
234
+ dataChannelTokenType || DataChannelTokenType.PracticeSession
235
+ );
236
+
237
+ return datachannelToken;
238
+ } catch (error) {
239
+ LoggerProxy.logger.warn(
240
+ `Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
241
+ error?.message || String(error)
242
+ }`
243
+ );
244
+
245
+ return undefined;
246
+ }
155
247
  },
156
248
 
157
249
  /**
@@ -160,7 +252,10 @@ const Webinar = WebexPlugin.extend({
160
252
  * @returns {Promise}
161
253
  */
162
254
  async updatePSDataChannel() {
163
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
255
+ this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
256
+ const invocationSequence = this._updatePSDataChannelSequence;
257
+
258
+ const meeting = this.getValidatedWebinarMeeting();
164
259
  const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
165
260
 
166
261
  if (!isPracticeSession) {
@@ -174,7 +269,7 @@ const Webinar = WebexPlugin.extend({
174
269
  meeting?.locusInfo || {};
175
270
 
176
271
  // @ts-ignore
177
- const practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
272
+ let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
178
273
  DataChannelTokenType.PracticeSession
179
274
  );
180
275
 
@@ -229,6 +324,29 @@ const Webinar = WebexPlugin.extend({
229
324
  this._pendingOnlineListener = null;
230
325
  }
231
326
 
327
+ const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
328
+
329
+ const latestPracticeSessionDatachannelUrl = get(
330
+ meeting,
331
+ 'locusInfo.info.practiceSessionDatachannelUrl'
332
+ );
333
+ const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
334
+
335
+ // Skip stale invocations after async refresh to avoid reconnecting a session
336
+ // that was already updated/cleaned by a newer state transition.
337
+ if (
338
+ invocationSequence !== this._updatePSDataChannelSequence ||
339
+ !isStillPracticeSession ||
340
+ !latestPracticeSessionDatachannelUrl ||
341
+ latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
342
+ ) {
343
+ return undefined;
344
+ }
345
+
346
+ if (refreshedPracticeSessionToken) {
347
+ practiceSessionDatachannelToken = refreshedPracticeSessionToken;
348
+ }
349
+
232
350
  // @ts-ignore - Fix type
233
351
  return this.webex.internal.llm
234
352
  .registerAndConnect(
@@ -238,15 +356,21 @@ const Webinar = WebexPlugin.extend({
238
356
  LLM_PRACTICE_SESSION
239
357
  )
240
358
  .then((registerAndConnectResult) => {
241
- // @ts-ignore - Fix type
242
- this.webex.internal.llm.off(
243
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
244
- meeting?.processRelayEvent
245
- );
359
+ // Track the exact listener reference so cleanupPSDataChannel can
360
+ // unsubscribe deterministically, even if the meeting can no longer
361
+ // be resolved at cleanup time.
362
+ if (this._practiceSessionRelayListener) {
363
+ // @ts-ignore - Fix type
364
+ this.webex.internal.llm.off(
365
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
366
+ this._practiceSessionRelayListener
367
+ );
368
+ }
369
+ this._practiceSessionRelayListener = meeting?.processRelayEvent;
246
370
  // @ts-ignore - Fix type
247
371
  this.webex.internal.llm.on(
248
372
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
249
- meeting?.processRelayEvent
373
+ this._practiceSessionRelayListener
250
374
  );
251
375
  // @ts-ignore - Fix type
252
376
  this.webex.internal.voicea?.announce?.();
@@ -267,6 +391,8 @@ const Webinar = WebexPlugin.extend({
267
391
  * @returns {Promise}
268
392
  */
269
393
  setPracticeSessionState(enabled) {
394
+ const meeting = this.getValidatedWebinarMeeting();
395
+
270
396
  return this.request({
271
397
  method: HTTP_VERBS.PATCH,
272
398
  uri: `${this.locusUrl}/controls`,
@@ -275,10 +401,16 @@ const Webinar = WebexPlugin.extend({
275
401
  enabled,
276
402
  },
277
403
  },
278
- }).catch((error) => {
279
- LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
280
- throw error;
281
- });
404
+ })
405
+ .then((response) => {
406
+ MeetingUtil.updateLocusFromApiResponse(meeting, response);
407
+
408
+ return response;
409
+ })
410
+ .catch((error) => {
411
+ LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
412
+ throw error;
413
+ });
282
414
  },
283
415
 
284
416
  /**
@@ -458,7 +590,14 @@ const Webinar = WebexPlugin.extend({
458
590
  * @returns {Promise}
459
591
  */
460
592
  async searchLargeScaleWebinarAttendees(payload) {
461
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
593
+ const meeting = this.getValidatedWebinarMeeting();
594
+ if (!meeting) {
595
+ LoggerProxy.logger.error(
596
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> webinar meeting could not be validated'
597
+ );
598
+ throw new Error('Meeting:webinar5k#Webinar meeting is not resolvable for the current locus');
599
+ }
600
+
462
601
  const rawParams = {
463
602
  search_text: payload?.queryString,
464
603
  limit: payload?.limit ?? DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
@@ -466,7 +605,9 @@ const Webinar = WebexPlugin.extend({
466
605
  };
467
606
  const attendeeSearchUrl = meeting?.locusInfo?.links?.resources?.attendeeSearch?.url;
468
607
  if (!attendeeSearchUrl) {
469
- LoggerProxy.logger.error(`Meeting:webinar5k#searchLargeScaleWebinarAttendees failed`);
608
+ LoggerProxy.logger.error(
609
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> attendee search url unavailable'
610
+ );
470
611
  throw new Error('Meeting:webinar5k#Attendee search url is not available');
471
612
  }
472
613
 
@@ -55,6 +55,27 @@ describe('plugin-meetings', () => {
55
55
  });
56
56
  });
57
57
 
58
+ describe('#locusUrlUpdate', () => {
59
+ it('should update the locusUrl property', () => {
60
+ const testLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id';
61
+
62
+ aiEnableRequest.locusUrlUpdate(testLocusUrl);
63
+
64
+ assert.equal(aiEnableRequest.locusUrl, testLocusUrl);
65
+ });
66
+
67
+ it('should handle updating locusUrl multiple times', () => {
68
+ const firstUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id-1';
69
+ const secondUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id-2';
70
+
71
+ aiEnableRequest.locusUrlUpdate(firstUrl);
72
+ assert.equal(aiEnableRequest.locusUrl, firstUrl);
73
+
74
+ aiEnableRequest.locusUrlUpdate(secondUrl);
75
+ assert.equal(aiEnableRequest.locusUrl, secondUrl);
76
+ });
77
+ });
78
+
58
79
  describe('#selfParticipantIdUpdate', () => {
59
80
  it('should update the selfParticipantId property', () => {
60
81
  const testSelfParticipantId = 'participant-123';
@@ -254,6 +275,71 @@ describe('plugin-meetings', () => {
254
275
  sinon.assert.notCalled(triggerSpy);
255
276
  });
256
277
 
278
+ it('should not trigger event when locusUrl does not match', () => {
279
+ const testLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id';
280
+ const differentLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/different-id';
281
+
282
+ aiEnableRequest.locusUrl = testLocusUrl;
283
+
284
+ // Reset the spy after setting locusUrl to avoid counting property change events
285
+ triggerSpy.resetHistory();
286
+
287
+ aiEnableRequest.listenToApprovalRequests();
288
+
289
+ const event = {
290
+ data: {
291
+ locusUrl: differentLocusUrl,
292
+ approval: {
293
+ resourceType: AI_ENABLE_REQUEST.RESOURCE_TYPE,
294
+ receivers: [{participantId: testSelfParticipantId}],
295
+ initiator: {participantId: testInitiatorId},
296
+ actionType: AI_ENABLE_REQUEST.ACTION_TYPE.REQUESTED,
297
+ url: testUrl,
298
+ },
299
+ },
300
+ };
301
+
302
+ webex.internal.mercury.emit(`event:${LOCUSEVENT.APPROVAL_REQUEST}`, event);
303
+
304
+ sinon.assert.notCalled(triggerSpy);
305
+ });
306
+
307
+ it('should trigger event when locusUrl matches', () => {
308
+ const testLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id';
309
+
310
+ aiEnableRequest.locusUrl = testLocusUrl;
311
+
312
+ // Reset the spy after setting locusUrl to avoid counting property change events
313
+ triggerSpy.resetHistory();
314
+
315
+ aiEnableRequest.listenToApprovalRequests();
316
+
317
+ const event = {
318
+ data: {
319
+ locusUrl: testLocusUrl,
320
+ approval: {
321
+ resourceType: AI_ENABLE_REQUEST.RESOURCE_TYPE,
322
+ receivers: [{participantId: testSelfParticipantId}],
323
+ initiator: {participantId: testInitiatorId},
324
+ actionType: AI_ENABLE_REQUEST.ACTION_TYPE.REQUESTED,
325
+ url: testUrl,
326
+ },
327
+ },
328
+ };
329
+
330
+ webex.internal.mercury.emit(`event:${LOCUSEVENT.APPROVAL_REQUEST}`, event);
331
+
332
+ sinon.assert.calledOnce(triggerSpy);
333
+ sinon.assert.calledWith(triggerSpy, AI_ENABLE_REQUEST.EVENTS.APPROVAL_REQUEST_ARRIVED, {
334
+ actionType: AI_ENABLE_REQUEST.ACTION_TYPE.REQUESTED,
335
+ isApprover: true,
336
+ isInitiator: false,
337
+ initiatorId: testInitiatorId,
338
+ approverId: testSelfParticipantId,
339
+ url: testUrl,
340
+ });
341
+ });
342
+
257
343
  it('should handle events with different action types', () => {
258
344
  aiEnableRequest.listenToApprovalRequests();
259
345
 
@@ -217,10 +217,14 @@ describe('plugin-meetings', () => {
217
217
  locusParticipantsUpdate: sinon.stub(),
218
218
  };
219
219
 
220
- const locusData = {some: 'data'};
220
+ const locusData = {participants: [{id: 'participant-1'}], sequence: {entries: [123]}};
221
221
  const result = breakout.parseRoster(locusData);
222
222
 
223
- assert.calledOnceWithExactly(breakout.members.locusParticipantsUpdate, locusData);
223
+ assert.calledOnceWithExactly(breakout.members.locusParticipantsUpdate, {
224
+ participants: [{id: 'participant-1'}],
225
+ isReplace: true,
226
+ });
227
+ assert.equal(breakout.breakoutRosterLocus, locusData);
224
228
  assert.equal(result, undefined);
225
229
  });
226
230
  it('not call locusParticipantsUpdate if sequence is expired', () => {
@@ -228,7 +232,7 @@ describe('plugin-meetings', () => {
228
232
  locusParticipantsUpdate: sinon.stub(),
229
233
  };
230
234
  breakout.isNeedHandleRoster = sinon.stub().returns(false);
231
- const locusData = {some: 'data'};
235
+ const locusData = {participants: [{id: 'participant-1'}], sequence: {entries: [123]}};
232
236
  breakout.parseRoster(locusData);
233
237
 
234
238
  assert.notCalled(breakout.members.locusParticipantsUpdate);