@webex/plugin-meetings 3.3.0 → 3.3.1

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 (35) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +4 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/mediaQualityMetrics/config.js +10 -10
  8. package/dist/mediaQualityMetrics/config.js.map +1 -1
  9. package/dist/meeting/index.js +21 -8
  10. package/dist/meeting/index.js.map +1 -1
  11. package/dist/meetings/index.js +6 -1
  12. package/dist/meetings/index.js.map +1 -1
  13. package/dist/reachability/index.js +82 -9
  14. package/dist/reachability/index.js.map +1 -1
  15. package/dist/statsAnalyzer/index.js +4 -2
  16. package/dist/statsAnalyzer/index.js.map +1 -1
  17. package/dist/statsAnalyzer/mqaUtil.js +14 -0
  18. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  19. package/dist/types/constants.d.ts +2 -1
  20. package/dist/types/mediaQualityMetrics/config.d.ts +8 -2
  21. package/dist/types/meeting/index.d.ts +8 -0
  22. package/dist/types/reachability/index.d.ts +11 -0
  23. package/dist/webinar/index.js +1 -1
  24. package/package.json +22 -22
  25. package/src/constants.ts +2 -1
  26. package/src/mediaQualityMetrics/config.ts +13 -7
  27. package/src/meeting/index.ts +23 -9
  28. package/src/meetings/index.ts +7 -2
  29. package/src/reachability/index.ts +57 -0
  30. package/src/statsAnalyzer/index.ts +6 -3
  31. package/src/statsAnalyzer/mqaUtil.ts +18 -0
  32. package/test/unit/spec/meeting/index.js +28 -8
  33. package/test/unit/spec/meetings/index.js +38 -15
  34. package/test/unit/spec/reachability/index.ts +266 -0
  35. package/test/unit/spec/stats-analyzer/index.js +89 -8
@@ -998,12 +998,11 @@ export class StatsAnalyzer extends EventsScope {
998
998
  result.qualityLimitationReason;
999
999
  this.statsResults[mediaType][sendrecvType].qualityLimitationResolutionChanges =
1000
1000
  result.qualityLimitationResolutionChanges;
1001
- this.statsResults[mediaType][sendrecvType].retransmittedPacketsSent =
1001
+ this.statsResults[mediaType][sendrecvType].totalRtxPacketsSent =
1002
1002
  result.retransmittedPacketsSent;
1003
+ this.statsResults[mediaType][sendrecvType].totalRtxBytesSent = result.retransmittedBytesSent;
1003
1004
  this.statsResults[mediaType][sendrecvType].totalBytesSent = result.bytesSent;
1004
1005
  this.statsResults[mediaType][sendrecvType].headerBytesSent = result.headerBytesSent;
1005
- this.statsResults[mediaType][sendrecvType].retransmittedBytesSent =
1006
- result.retransmittedBytesSent;
1007
1006
  this.statsResults[mediaType][sendrecvType].requestedBitrate = result.requestedBitrate;
1008
1007
  this.statsResults[mediaType][sendrecvType].requestedFrameSize = result.requestedFrameSize;
1009
1008
  }
@@ -1136,6 +1135,10 @@ export class StatsAnalyzer extends EventsScope {
1136
1135
  this.statsResults[mediaType][sendrecvType].fecPacketsReceived = result.fecPacketsReceived;
1137
1136
  this.statsResults[mediaType][sendrecvType].totalBytesReceived = result.bytesReceived;
1138
1137
  this.statsResults[mediaType][sendrecvType].headerBytesReceived = result.headerBytesReceived;
1138
+ this.statsResults[mediaType][sendrecvType].totalRtxPacketsReceived =
1139
+ result.retransmittedPacketsReceived;
1140
+ this.statsResults[mediaType][sendrecvType].totalRtxBytesReceived =
1141
+ result.retransmittedBytesReceived;
1139
1142
 
1140
1143
  this.statsResults[mediaType][sendrecvType].meanRtpJitter.push(result.jitter);
1141
1144
 
@@ -237,10 +237,16 @@ export const getVideoReceiverMqa = ({
237
237
  const lastPacketsLost = getLastTotalValue('totalPacketsLost');
238
238
  const lastBytesReceived = getLastTotalValue('totalBytesReceived');
239
239
 
240
+ const lastRtxPacketsReceived = getLastTotalValue('totalRtxPacketsReceived');
241
+ const lastRtxBytesReceived = getLastTotalValue('totalRtxBytesReceived');
242
+
240
243
  const packetsLost = getTotalValue('totalPacketsLost');
241
244
  const totalPacketsReceived = getTotalValue('totalPacketsReceived');
242
245
  const totalBytesReceived = getTotalValue('totalBytesReceived');
243
246
 
247
+ const totalRtxPacketsReceived = getTotalValue('totalRtxPacketsReceived');
248
+ const totalRtxBytesReceived = getTotalValue('totalRtxBytesReceived');
249
+
244
250
  const meanRemoteJitter = Object.keys(statsResults)
245
251
  .filter((mt) => mt.includes(baseMediaType))
246
252
  .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRemoteJitter), []);
@@ -266,10 +272,15 @@ export const getVideoReceiverMqa = ({
266
272
 
267
273
  // Calculate the outgoing bitrate
268
274
  const totalBytesReceivedInaMin = totalBytesReceived - lastBytesReceived;
275
+ const totalRtxBytesReceivedInaMin = totalRtxBytesReceived - lastRtxBytesReceived;
269
276
 
270
277
  videoReceiver.common.rtpBitrate = totalBytesReceivedInaMin
271
278
  ? (totalBytesReceivedInaMin * 8) / 60
272
279
  : 0;
280
+ videoReceiver.common.rtxPackets = totalRtxPacketsReceived - lastRtxPacketsReceived;
281
+ videoReceiver.common.rtxBitrate = totalRtxBytesReceivedInaMin
282
+ ? (totalRtxBytesReceivedInaMin * 8) / 60
283
+ : 0;
273
284
  };
274
285
 
275
286
  export const getVideoReceiverStreamMqa = ({
@@ -349,11 +360,15 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, b
349
360
  const lastPacketsSent = getLastTotalValue('totalPacketsSent');
350
361
  const lastBytesSent = getLastTotalValue('totalBytesSent');
351
362
  const lastPacketsLostTotal = getLastTotalValue('totalPacketsLostOnReceiver');
363
+ const lastRtxPacketsSent = getLastTotalValue('totalRtxPacketsSent');
364
+ const lastRtxBytesSent = getLastTotalValue('totalRtxBytesSent');
352
365
 
353
366
  const totalPacketsLostOnReceiver = getTotalValue('totalPacketsLostOnReceiver');
354
367
  const totalPacketsSent = getTotalValue('totalPacketsSent');
355
368
  const totalBytesSent = getTotalValue('totalBytesSent');
356
369
  const availableOutgoingBitrate = getTotalValue('availableOutgoingBitrate');
370
+ const totalRtxPacketsSent = getTotalValue('totalRtxPacketsSent');
371
+ const totalRtxBytesSent = getTotalValue('totalRtxBytesSent');
357
372
 
358
373
  videoSender.common.common.direction =
359
374
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
@@ -389,8 +404,11 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, b
389
404
 
390
405
  // Calculate the outgoing bitrate
391
406
  const totalBytesSentInaMin = totalBytesSent - lastBytesSent;
407
+ const totalRtxBytesSentInaMin = totalRtxBytesSent - lastRtxBytesSent;
392
408
 
393
409
  videoSender.common.rtpBitrate = totalBytesSentInaMin ? (totalBytesSentInaMin * 8) / 60 : 0;
410
+ videoSender.common.rtxPackets = totalRtxPacketsSent - lastRtxPacketsSent;
411
+ videoSender.common.rtxBitrate = totalRtxBytesSentInaMin ? (totalRtxBytesSentInaMin * 8) / 60 : 0;
394
412
  };
395
413
 
396
414
  export const getVideoSenderStreamMqa = ({
@@ -1302,6 +1302,31 @@ describe('plugin-meetings', () => {
1302
1302
  );
1303
1303
  });
1304
1304
  });
1305
+
1306
+ describe('#handleLLMOnline', () => {
1307
+ beforeEach(() => {
1308
+ webex.internal.llm.off = sinon.stub();
1309
+ });
1310
+
1311
+ it('turns off llm online, emits transcription connected events', () => {
1312
+ meeting.handleLLMOnline();
1313
+ assert.calledOnceWithExactly(
1314
+ webex.internal.llm.off,
1315
+ 'online',
1316
+ meeting.handleLLMOnline
1317
+ );
1318
+ assert.calledWith(
1319
+ TriggerProxy.trigger,
1320
+ sinon.match.instanceOf(Meeting),
1321
+ {
1322
+ file: 'meeting/index',
1323
+ function: 'handleLLMOnline',
1324
+ },
1325
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1326
+ );
1327
+ });
1328
+ });
1329
+
1305
1330
  describe('#join', () => {
1306
1331
  let sandbox = null;
1307
1332
  let setCorrelationIdSpy;
@@ -1351,15 +1376,10 @@ describe('plugin-meetings', () => {
1351
1376
  assert.calledOnce(MeetingUtil.joinMeeting);
1352
1377
  assert.calledOnce(meeting.setLocus);
1353
1378
  assert.equal(result, joinMeetingResult);
1354
-
1355
1379
  assert.calledWith(
1356
- TriggerProxy.trigger,
1357
- sinon.match.instanceOf(Meeting),
1358
- {
1359
- file: 'meeting/index',
1360
- function: 'join',
1361
- },
1362
- EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1380
+ webex.internal.llm.on,
1381
+ 'online',
1382
+ meeting.handleLLMOnline
1363
1383
  );
1364
1384
  });
1365
1385
 
@@ -18,6 +18,7 @@ import TriggerProxy from '@webex/plugin-meetings/src/common/events/trigger-proxy
18
18
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
19
19
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
20
20
  import Meeting, {CallStateForMetrics} from '@webex/plugin-meetings/src/meeting';
21
+ import {Services} from '@webex/webex-core';
21
22
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
22
23
  import Meetings from '@webex/plugin-meetings/src/meetings';
23
24
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
@@ -75,6 +76,8 @@ describe('plugin-meetings', () => {
75
76
  let test1;
76
77
  let test2;
77
78
  let locusInfo;
79
+ let services;
80
+ let catalog;
78
81
 
79
82
  describe('meetings index', () => {
80
83
  beforeEach(() => {
@@ -93,9 +96,13 @@ describe('plugin-meetings', () => {
93
96
  device: Device,
94
97
  mercury: Mercury,
95
98
  meetings: Meetings,
99
+ services: Services,
96
100
  },
97
101
  });
98
102
 
103
+ services = webex.internal.services;
104
+ catalog = services._getCatalog();
105
+
99
106
  Object.assign(webex, {
100
107
  logging: logger,
101
108
  });
@@ -161,6 +168,7 @@ describe('plugin-meetings', () => {
161
168
  ],
162
169
  })
163
170
  ),
171
+ _getCatalog: sinon.stub().returns(catalog),
164
172
  fetchClientRegionInfo: sinon.stub().returns(Promise.resolve()),
165
173
  },
166
174
  metrics: {
@@ -1917,34 +1925,34 @@ describe('plugin-meetings', () => {
1917
1925
  let loggerProxySpy;
1918
1926
 
1919
1927
  it('should call request.getMeetingPreferences to get the preferred webex site ', async () => {
1928
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
1920
1929
  assert.isDefined(webex.meetings.preferredWebexSite);
1921
1930
  await webex.meetings.fetchUserPreferredWebexSite();
1922
1931
 
1923
1932
  assert.equal(webex.meetings.preferredWebexSite, 'go.webex.com');
1933
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), [
1934
+ 'go.webex.com',
1935
+ ]);
1924
1936
  });
1925
1937
 
1926
1938
  const setup = ({user} = {}) => {
1927
1939
  loggerProxySpy = sinon.spy(LoggerProxy.logger, 'error');
1940
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
1928
1941
 
1929
1942
  Object.assign(webex.internal, {
1930
- services: {
1931
- getMeetingPreferences: sinon.stub().returns(Promise.resolve({})),
1932
- },
1933
1943
  user: {
1934
1944
  get: sinon.stub().returns(Promise.resolve(user)),
1935
1945
  },
1936
1946
  });
1947
+
1948
+ Object.assign(webex.internal.services, {
1949
+ getMeetingPreferences: sinon.stub().returns(Promise.resolve({})),
1950
+ });
1937
1951
  };
1938
1952
 
1939
1953
  it('should not fail if UserPreferred info is not fetched ', async () => {
1940
1954
  setup();
1941
1955
 
1942
- Object.assign(webex.internal, {
1943
- services: {
1944
- getMeetingPreferences: sinon.stub().returns(Promise.resolve({})),
1945
- },
1946
- });
1947
-
1948
1956
  await webex.meetings.fetchUserPreferredWebexSite().then(() => {
1949
1957
  assert.equal(webex.meetings.preferredWebexSite, '');
1950
1958
  });
@@ -1952,6 +1960,7 @@ describe('plugin-meetings', () => {
1952
1960
  loggerProxySpy,
1953
1961
  'Failed to fetch preferred site from user - no site will be set'
1954
1962
  );
1963
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
1955
1964
  });
1956
1965
 
1957
1966
  it('should fall back to fetching the site from the user', async () => {
@@ -1968,6 +1977,10 @@ describe('plugin-meetings', () => {
1968
1977
  await webex.meetings.fetchUserPreferredWebexSite();
1969
1978
 
1970
1979
  assert.equal(webex.meetings.preferredWebexSite, 'site.webex.com');
1980
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), [
1981
+ '',
1982
+ 'site.webex.com',
1983
+ ]);
1971
1984
  assert.notCalled(loggerProxySpy);
1972
1985
  });
1973
1986
 
@@ -1989,6 +2002,7 @@ describe('plugin-meetings', () => {
1989
2002
  loggerProxySpy,
1990
2003
  'Failed to fetch preferred site from user - no site will be set'
1991
2004
  );
2005
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
1992
2006
  });
1993
2007
  }
1994
2008
  );
@@ -2005,6 +2019,7 @@ describe('plugin-meetings', () => {
2005
2019
  loggerProxySpy,
2006
2020
  'Failed to fetch preferred site from user - no site will be set'
2007
2021
  );
2022
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
2008
2023
  });
2009
2024
 
2010
2025
  it('should fall back to fetching the site from the user', async () => {
@@ -2022,6 +2037,10 @@ describe('plugin-meetings', () => {
2022
2037
 
2023
2038
  assert.equal(webex.meetings.preferredWebexSite, 'site.webex.com');
2024
2039
  assert.notCalled(loggerProxySpy);
2040
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), [
2041
+ '',
2042
+ 'site.webex.com',
2043
+ ]);
2025
2044
  });
2026
2045
 
2027
2046
  forEach(
@@ -2042,6 +2061,7 @@ describe('plugin-meetings', () => {
2042
2061
  loggerProxySpy,
2043
2062
  'Failed to fetch preferred site from user - no site will be set'
2044
2063
  );
2064
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
2045
2065
  });
2046
2066
  }
2047
2067
  );
@@ -2058,6 +2078,7 @@ describe('plugin-meetings', () => {
2058
2078
  loggerProxySpy,
2059
2079
  'Failed to fetch preferred site from user - no site will be set'
2060
2080
  );
2081
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
2061
2082
  });
2062
2083
  });
2063
2084
  });
@@ -2344,12 +2365,14 @@ describe('plugin-meetings', () => {
2344
2365
  sessionType: 'MAIN',
2345
2366
  };
2346
2367
  newLocus.self.state = 'JOINED';
2347
- newLocus.self.devices = [{
2348
- intent: {
2349
- reason: 'ON_HOLD_LOBBY',
2350
- type: 'WAIT',
2351
- }
2352
- }];
2368
+ newLocus.self.devices = [
2369
+ {
2370
+ intent: {
2371
+ reason: 'ON_HOLD_LOBBY',
2372
+ type: 'WAIT',
2373
+ },
2374
+ },
2375
+ ];
2353
2376
  LoggerProxy.logger.log = sinon.stub();
2354
2377
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
2355
2378
  assert.equal(result, true);
@@ -141,6 +141,272 @@ describe('isAnyPublicClusterReachable', () => {
141
141
  });
142
142
  });
143
143
 
144
+
145
+ describe('isWebexMediaBackendUnreachable', () => {
146
+ let webex;
147
+
148
+ beforeEach(() => {
149
+ webex = new MockWebex();
150
+
151
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
152
+ });
153
+
154
+ afterEach(() => {
155
+ sinon.restore();
156
+ });
157
+
158
+ const runCheck = async (mockStorage: any, expectedValue: boolean) => {
159
+ if (mockStorage) {
160
+ await webex.boundedStorage.put(
161
+ 'Reachability',
162
+ 'reachability.result',
163
+ JSON.stringify(mockStorage)
164
+ );
165
+ }
166
+ const reachability = new Reachability(webex);
167
+
168
+ const result = await reachability.isWebexMediaBackendUnreachable();
169
+
170
+ assert.equal(result, expectedValue);
171
+ };
172
+
173
+ [
174
+ {
175
+ title: 'no clusters at all',
176
+ mockStorage: {},
177
+ expectedResult: false,
178
+ },
179
+ {
180
+ title: 'clusters without results',
181
+ mockStorage: {a: {}, b: {}},
182
+ expectedResult: false,
183
+ },
184
+ {
185
+ title: 'all clusters untested',
186
+ mockStorage: {
187
+ a: {udp: 'untested'},
188
+ b: {udp: 'untested', tcp: 'untested'},
189
+ },
190
+ expectedResult: false,
191
+ },
192
+ {
193
+ title: 'one cluster with udp reachable',
194
+ mockStorage: {x: {udp: {result: 'reachable'}, tcp: {result: 'unreachable'}}},
195
+ expectedResult: false,
196
+ },
197
+ {
198
+ title: 'one cluster with tcp reachable',
199
+ mockStorage: {x: {tcp: {result: 'reachable'}}},
200
+ expectedResult: false,
201
+ },
202
+ {
203
+ title: 'one cluster with xtls reachable',
204
+ mockStorage: {x: {xtls: {result: 'reachable'}}, y: {xtls: {result: 'unreachable'}}},
205
+ expectedResult: false,
206
+ },
207
+ {
208
+ title: 'multiple clusters with various protocols reachable',
209
+ mockStorage: {
210
+ a: {udp: {result: 'reachable'}, tcp: {result: 'reachable'}},
211
+ b: {udp: {result: 'unreachable'}, tcp: {result: 'reachable'}},
212
+ c: {tcp: {result: 'reachable'}},
213
+ d: {xtls: {result: 'reachable'}},
214
+ },
215
+ expectedResult: false,
216
+ },
217
+ {
218
+ title: 'multiple clusters with all protocols unreachable',
219
+ mockStorage: {
220
+ a: {
221
+ udp: {result: 'unreachable'},
222
+ tcp: {result: 'unreachable'},
223
+ xtls: {result: 'unreachable'},
224
+ },
225
+ b: {
226
+ udp: {result: 'unreachable'},
227
+ tcp: {result: 'unreachable'},
228
+ xtls: {result: 'unreachable'},
229
+ },
230
+ c: {
231
+ udp: {result: 'unreachable'},
232
+ tcp: {result: 'unreachable'},
233
+ xtls: {result: 'unreachable'},
234
+ },
235
+ },
236
+ expectedResult: true,
237
+ },
238
+ {
239
+ title: 'multiple clusters with UDP and TCP protocols unreachable, but TLS not tested',
240
+ mockStorage: {
241
+ a: {
242
+ udp: {result: 'unreachable'},
243
+ tcp: {result: 'unreachable'},
244
+ xtls: {result: 'untested'},
245
+ },
246
+ b: {
247
+ udp: {result: 'unreachable'},
248
+ tcp: {result: 'unreachable'},
249
+ xtls: {result: 'untested'},
250
+ },
251
+ c: {
252
+ udp: {result: 'unreachable'},
253
+ tcp: {result: 'unreachable'},
254
+ xtls: {result: 'untested'},
255
+ },
256
+ },
257
+ expectedResult: false,
258
+ },
259
+ {
260
+ title: 'multiple clusters with UDP and TCP protocols unreachable, but TLS missing',
261
+ mockStorage: {
262
+ a: {
263
+ udp: {result: 'unreachable'},
264
+ tcp: {result: 'unreachable'},
265
+ },
266
+ b: {
267
+ udp: {result: 'unreachable'},
268
+ tcp: {result: 'unreachable'},
269
+ },
270
+ c: {
271
+ udp: {result: 'unreachable'},
272
+ tcp: {result: 'unreachable'},
273
+ },
274
+ },
275
+ expectedResult: false,
276
+ },
277
+ {
278
+ title: 'multiple clusters with UDP and TLS protocols unreachable, but TCP not tested',
279
+ mockStorage: {
280
+ a: {
281
+ udp: {result: 'unreachable'},
282
+ tcp: {result: 'untested'},
283
+ xtls: {result: 'unreachable'},
284
+ },
285
+ b: {
286
+ udp: {result: 'unreachable'},
287
+ tcp: {result: 'untested'},
288
+ xtls: {result: 'unreachable'},
289
+ },
290
+ c: {
291
+ udp: {result: 'unreachable'},
292
+ tcp: {result: 'untested'},
293
+ xtls: {result: 'unreachable'},
294
+ },
295
+ },
296
+ expectedResult: false,
297
+ },
298
+ {
299
+ title: 'multiple clusters with UDP and TLS protocols unreachable, but TCP missing',
300
+ mockStorage: {
301
+ a: {
302
+ udp: {result: 'unreachable'},
303
+ xtls: {result: 'unreachable'},
304
+ },
305
+ b: {
306
+ udp: {result: 'unreachable'},
307
+ xtls: {result: 'unreachable'},
308
+ },
309
+ c: {
310
+ udp: {result: 'unreachable'},
311
+ xtls: {result: 'unreachable'},
312
+ },
313
+ },
314
+ expectedResult: false,
315
+ },
316
+ {
317
+ title: 'multiple clusters with all protocols unreachable, some untested',
318
+ mockStorage: {
319
+ a: {
320
+ udp: {result: 'unreachable'},
321
+ tcp: {result: 'unreachable'},
322
+ xtls: {result: 'unreachable'},
323
+ },
324
+ b: {udp: {result: 'unreachable'}, tcp: {result: 'untested'}, xtls: {result: 'unreachable'}},
325
+ c: {udp: {result: 'unreachable'}, tcp: {result: 'unreachable'}, xtls: {result: 'untested'}},
326
+ },
327
+ expectedResult: true,
328
+ },
329
+ {
330
+ title: 'multiple clusters with all protocols unreachable, except for 1 reachable on udp',
331
+ mockStorage: {
332
+ a: {
333
+ udp: {result: 'reachable'},
334
+ tcp: {result: 'unreachable'},
335
+ xtls: {result: 'unreachable'},
336
+ },
337
+ b: {
338
+ udp: {result: 'unreachable'},
339
+ tcp: {result: 'unreachable'},
340
+ xtls: {result: 'unreachable'},
341
+ },
342
+ c: {
343
+ udp: {result: 'unreachable'},
344
+ tcp: {result: 'unreachable'},
345
+ xtls: {result: 'unreachable'},
346
+ },
347
+ },
348
+ expectedResult: false,
349
+ },
350
+ {
351
+ title: 'multiple clusters with all protocols unreachable, except for 1 reachable on tcp',
352
+ mockStorage: {
353
+ a: {
354
+ udp: {result: 'unreachable'},
355
+ tcp: {result: 'unreachable'},
356
+ xtls: {result: 'unreachable'},
357
+ },
358
+ b: {
359
+ udp: {result: 'unreachable'},
360
+ tcp: {result: 'unreachable'},
361
+ xtls: {result: 'unreachable'},
362
+ },
363
+ c: {
364
+ udp: {result: 'unreachable'},
365
+ tcp: {result: 'reachable'},
366
+ xtls: {result: 'unreachable'},
367
+ },
368
+ },
369
+ expectedResult: false,
370
+ },
371
+ {
372
+ title: 'multiple clusters with all protocols unreachable, except for 1 reachable on xtls',
373
+ mockStorage: {
374
+ a: {
375
+ udp: {result: 'unreachable'},
376
+ tcp: {result: 'unreachable'},
377
+ xtls: {result: 'unreachable'},
378
+ },
379
+ b: {
380
+ udp: {result: 'unreachable'},
381
+ tcp: {result: 'unreachable'},
382
+ xtls: {result: 'reachable'},
383
+ },
384
+ c: {
385
+ udp: {result: 'unreachable'},
386
+ tcp: {result: 'unreachable'},
387
+ xtls: {result: 'unreachable'},
388
+ },
389
+ },
390
+ expectedResult: false,
391
+ },
392
+ {
393
+ title: 'multiple clusters with some missing results',
394
+ mockStorage: {
395
+ a: {udp: {result: 'unreachable'}},
396
+ b: {tcp: {result: 'unreachable'}},
397
+ c: {xtls: {result: 'unreachable'}},
398
+ d: {},
399
+ },
400
+ expectedResult: true,
401
+ },
402
+ ].forEach(({mockStorage, expectedResult, title}) => {
403
+ it(`returns ${expectedResult} when ${title}`, async () => {
404
+ await runCheck(mockStorage, expectedResult);
405
+ });
406
+ });
407
+ });
408
+
409
+
144
410
  describe('gatherReachability', () => {
145
411
  let webex;
146
412