@webex/internal-plugin-llm 3.11.0 → 3.12.0-next.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.
@@ -19,47 +19,182 @@ describe('plugin-llm', () => {
19
19
  },
20
20
  });
21
21
 
22
+ webex.internal.feature = {
23
+ setFeature: sinon.stub().resolves({value: true}),
24
+ getFeature: sinon.stub().resolves(true),
25
+ };
26
+
22
27
  llmService = webex.internal.llm;
23
- llmService.connect = sinon.stub().callsFake(() => {
24
- llmService.connected = true;
25
- });
28
+ llmService.webSocketUrl = 'wss://example.com/socket';
26
29
  llmService.disconnect = sinon.stub().resolves(true);
27
30
  llmService.request = sinon.stub().resolves({
28
31
  headers: {},
29
32
  body: {
30
33
  binding: 'binding',
31
- webSocketUrl: 'url',
34
+ webSocketUrl: 'wss://example.com/socket',
32
35
  },
33
36
  });
37
+ const sockets = new Map();
38
+
39
+ llmService.connect = sinon.stub().callsFake((url, sessionId) => {
40
+ sockets.set(sessionId, {connected: true});
41
+ llmService.getSocket = sinon.stub().callsFake((sid) => sockets.get(sid));
42
+ });
43
+ llmService.connections.set('llm-default-session',{
44
+ webSocketUrl: 'wss://example.com/socket',
45
+ })
34
46
  });
35
47
 
48
+ afterEach(() => sinon.restore());
49
+
36
50
  describe('#registerAndConnect', () => {
37
51
  it('registers connection', async () => {
38
- llmService.register = sinon.stub().resolves({
39
- body: {
40
- binding: 'binding',
41
- webSocketUrl: 'url',
42
- },
52
+ llmService.register = sinon.stub().callsFake(async () => {
53
+ llmService.binding = 'binding';
54
+ llmService.webSocketUrl = 'wss://example.com/socket';
55
+ return {
56
+ body: {
57
+ binding: 'binding',
58
+ webSocketUrl: 'wss://example.com/socket',
59
+ },
60
+ };
43
61
  });
44
- assert.equal(llmService.isConnected(), false);
45
- await llmService.registerAndConnect(locusUrl, datachannelUrl);
46
- assert.equal(llmService.isConnected(), true);
62
+
63
+ assert.equal(llmService.isConnected('llm-default-session'), false);
64
+ await llmService.registerAndConnect(locusUrl, datachannelUrl,undefined);
65
+ assert.equal(llmService.isConnected('llm-default-session'), true);
47
66
  });
48
67
 
49
- it("doesn't registers connection for invalid input", async () => {
50
- llmService.register = sinon.stub().resolves({
51
- body: {
52
- binding: 'binding',
53
- webSocketUrl: 'url',
54
- },
68
+ it("doesn't register connection for invalid input", async () => {
69
+ llmService.register = sinon.stub().callsFake(async () => {
70
+ llmService.binding = 'binding';
71
+ llmService.webSocketUrl = 'wss://example.com/socket';
72
+ return {
73
+ body: {
74
+ binding: 'binding',
75
+ webSocketUrl: 'wss://example.com/socket',
76
+ },
77
+ };
55
78
  });
79
+
56
80
  await llmService.registerAndConnect();
57
81
  assert.equal(llmService.isConnected(), false);
58
82
  });
83
+
84
+ it('registers connection with token', async () => {
85
+ llmService.register = sinon.stub().callsFake(async () => {
86
+ llmService.binding = 'binding';
87
+ llmService.webSocketUrl = 'wss://example.com/socket';
88
+ return {
89
+ body: {
90
+ binding: 'binding',
91
+ webSocketUrl: 'wss://example.com/socket',
92
+ },
93
+ };
94
+ });
95
+
96
+ assert.equal(llmService.isConnected(), false);
97
+
98
+ await llmService.registerAndConnect(locusUrl, datachannelUrl,'abc123');
99
+
100
+ sinon.assert.calledOnceWithExactly(
101
+ llmService.register,
102
+ datachannelUrl,
103
+ 'abc123',
104
+ 'llm-default-session'
105
+ );
106
+
107
+ assert.equal(llmService.isConnected(), true);
108
+ });
109
+
110
+ it('connects with subscriptionAwareSubchannels when token enabled', async () => {
111
+ llmService.isDataChannelTokenEnabled = sinon.stub().returns(true);
112
+
113
+ llmService.register = sinon.stub().callsFake(async () => {
114
+ llmService.binding = 'binding';
115
+ llmService.webSocketUrl = 'wss://example.com/socket';
116
+ return {
117
+ body: {
118
+ binding: 'binding',
119
+ webSocketUrl: 'wss://example.com/socket',
120
+ },
121
+ };
122
+ });
123
+
124
+ const buildSpy = sinon.spy(LLMService, 'buildUrlWithAwareSubchannels');
125
+
126
+ await llmService.registerAndConnect(locusUrl, datachannelUrl,'abc123');
127
+
128
+ sinon.assert.calledOnce(buildSpy);
129
+ sinon.assert.calledOnce(llmService.connect);
130
+
131
+ const calledUrl = llmService.connect.getCall(0).args[0];
132
+ assert.include(calledUrl, 'subscriptionAwareSubchannels=');
133
+ });
134
+
135
+ it('connects without subscriptionAwareSubchannels when token disabled', async () => {
136
+ llmService.isDataChannelTokenEnabled = sinon.stub().returns(false);
137
+
138
+ llmService.register = sinon.stub().callsFake(async () => {
139
+ llmService.binding = 'binding';
140
+ llmService.webSocketUrl = 'wss://example.com/socket';
141
+ return {
142
+ body: {
143
+ binding: 'binding',
144
+ webSocketUrl: 'wss://example.com/socket',
145
+ },
146
+ };
147
+ });
148
+
149
+ const buildSpy = sinon.spy(LLMService, 'buildUrlWithAwareSubchannels');
150
+
151
+ await llmService.registerAndConnect(locusUrl, datachannelUrl);
152
+
153
+ sinon.assert.notCalled(buildSpy);
154
+ sinon.assert.calledOnce(llmService.connect);
155
+
156
+ const calledUrl = llmService.connect.getCall(0).args[0];
157
+ assert.equal(calledUrl, llmService.webSocketUrl);
158
+ });
159
+
160
+ it('connects without subscriptionAwareSubchannels when token enabled BUT token missing', async () => {
161
+ llmService.isDataChannelTokenEnabled = sinon.stub().resolves(true);
162
+
163
+ const buildSpy = sinon.spy(LLMService, 'buildUrlWithAwareSubchannels');
164
+
165
+ await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined);
166
+
167
+ sinon.assert.calledOnce(buildSpy);
168
+ sinon.assert.calledOnce(llmService.connect);
169
+
170
+ const calledUrl = llmService.connect.getCall(0).args[0];
171
+ assert.include(calledUrl, 'subscriptionAwareSubchannels=');
172
+
173
+ buildSpy.restore();
174
+ });
59
175
  });
60
176
 
61
177
  describe('#register', () => {
62
- it('registers connection', async () => {
178
+ beforeEach(() => {
179
+ llmService.isDataChannelTokenEnabled = sinon.stub();
180
+ });
181
+
182
+ it('registers connection with token header', async () => {
183
+ llmService.isDataChannelTokenEnabled.resolves(true);
184
+ await llmService.register(datachannelUrl, 'abc123');
185
+
186
+ sinon.assert.calledOnceWithExactly(
187
+ llmService.request,
188
+ sinon.match({
189
+ method: 'POST',
190
+ url: `${datachannelUrl}`,
191
+ body: {deviceUrl: webex.internal.device.url},
192
+ headers: {'Data-Channel-Auth-Token': 'abc123'},
193
+ })
194
+ );
195
+ });
196
+
197
+ it('registers connection without token header when none provided', async () => {
63
198
  await llmService.register(datachannelUrl);
64
199
 
65
200
  sinon.assert.calledOnceWithExactly(
@@ -68,21 +203,40 @@ describe('plugin-llm', () => {
68
203
  method: 'POST',
69
204
  url: `${datachannelUrl}`,
70
205
  body: {deviceUrl: webex.internal.device.url},
206
+ headers: {},
71
207
  })
72
208
  );
209
+ });
73
210
 
74
- assert.equal(llmService.getBinding(), 'binding');
211
+ it('registers connection without token header when toggle disabled', async () => {
212
+ llmService.isDataChannelTokenEnabled.resolves(false);
213
+
214
+ await llmService.register(datachannelUrl,'abc123');
215
+ sinon.assert.calledOnceWithExactly(
216
+ llmService.request,
217
+ sinon.match({
218
+ method: 'POST',
219
+ url: `${datachannelUrl}`,
220
+ body: {deviceUrl: webex.internal.device.url},
221
+ headers: {},
222
+ })
223
+ );
75
224
  });
76
225
  });
77
226
 
78
227
  describe('#getLocusUrl', () => {
79
228
  it('gets LocusUrl', async () => {
80
- llmService.register = sinon.stub().resolves({
81
- body: {
82
- binding: 'binding',
83
- webSocketUrl: 'url',
84
- },
229
+ llmService.register = sinon.stub().callsFake(async () => {
230
+ llmService.binding = 'binding';
231
+ llmService.webSocketUrl = 'wss://example.com/socket';
232
+ return {
233
+ body: {
234
+ binding: 'binding',
235
+ webSocketUrl: 'wss://example.com/socket',
236
+ },
237
+ };
85
238
  });
239
+
86
240
  await llmService.registerAndConnect(locusUrl, datachannelUrl);
87
241
  assert.equal(llmService.getLocusUrl(), locusUrl);
88
242
  });
@@ -90,11 +244,15 @@ describe('plugin-llm', () => {
90
244
 
91
245
  describe('#getDatachannelUrl', () => {
92
246
  it('gets dataChannel Url', async () => {
93
- llmService.register = sinon.stub().resolves({
94
- body: {
95
- binding: 'binding',
96
- webSocketUrl: 'url',
97
- },
247
+ llmService.register = sinon.stub().callsFake(async () => {
248
+ llmService.binding = 'binding';
249
+ llmService.webSocketUrl = 'wss://example.com/socket';
250
+ return {
251
+ body: {
252
+ binding: 'binding',
253
+ webSocketUrl: 'wss://example.com/socket',
254
+ },
255
+ };
98
256
  });
99
257
  await llmService.registerAndConnect(locusUrl, datachannelUrl);
100
258
  assert.equal(llmService.getDatachannelUrl(), datachannelUrl);
@@ -112,42 +270,181 @@ describe('plugin-llm', () => {
112
270
  });
113
271
  });
114
272
 
115
- describe('disconnectLLM', () => {
273
+ describe('#disconnectLLM', () => {
116
274
  let instance;
117
275
 
118
276
  beforeEach(() => {
119
277
  instance = {
120
278
  disconnect: jest.fn(() => Promise.resolve()),
121
- locusUrl: 'someUrl',
122
- datachannelUrl: 'someUrl',
123
- binding: {},
124
- webSocketUrl: 'someUrl',
125
- disconnectLLM: function (options) {
126
- return this.disconnect(options).then(() => {
127
- this.locusUrl = undefined;
128
- this.datachannelUrl = undefined;
129
- this.binding = undefined;
130
- this.webSocketUrl = undefined;
279
+ connections: new Map([
280
+ ['llm-default-session', { foo: 'bar' }],
281
+ ]),
282
+ datachannelTokens: {
283
+ 'llm-default-session': 'session-token',
284
+ },
285
+
286
+ disconnectLLM: function (options, sessionId = 'llm-default-session') {
287
+ return this.disconnect(options, sessionId).then(() => {
288
+ this.connections.delete(sessionId);
289
+ this.datachannelTokens[sessionId] = undefined;
131
290
  });
132
- }
291
+ },
133
292
  };
134
293
  });
135
294
 
136
- it('should call disconnect and clear relevant properties', async () => {
137
- await instance.disconnectLLM({});
295
+ it('calls disconnect and clears session connection + token', async () => {
296
+ await instance.disconnectLLM({ code: 3000, reason: 'bye' });
297
+
298
+ expect(instance.disconnect).toHaveBeenCalledWith(
299
+ { code: 3000, reason: 'bye' },
300
+ 'llm-default-session'
301
+ );
302
+
303
+ expect(instance.connections.has('llm-default-session')).toBe(false);
304
+
305
+ expect(instance.datachannelTokens['llm-default-session']).toBeUndefined();
306
+ });
307
+
308
+ it('propagates disconnect errors', async () => {
309
+ instance.disconnect.mockRejectedValue(new Error('disconnect failed'));
138
310
 
139
- expect(instance.disconnect).toHaveBeenCalledWith({});
140
- expect(instance.locusUrl).toBeUndefined();
141
- expect(instance.datachannelUrl).toBeUndefined();
142
- expect(instance.binding).toBeUndefined();
143
- expect(instance.webSocketUrl).toBeUndefined();
311
+ await expect(
312
+ instance.disconnectLLM({ code: 3000, reason: 'bye' })
313
+ ).rejects.toThrow('disconnect failed');
144
314
  });
315
+ });
316
+
317
+ describe('#setRefreshHandler', () => {
318
+ it('stores the provided handler', () => {
319
+ const handler = sinon.stub().resolves({ body: { datachannelToken: 'newToken' } });
320
+ llmService.setRefreshHandler(handler);
321
+
322
+ // @ts-ignore
323
+ assert.equal(llmService.refreshHandler, handler);
324
+ });
325
+ });
145
326
 
146
- it('should handle errors from disconnect gracefully', async () => {
147
- instance.disconnect.mockRejectedValue(new Error('Disconnect failed'));
327
+ describe('#isDataChannelTokenEnabled', () => {
328
+ it('works correctly', async () => {
329
+ webex.internal.feature.getFeature.returns(true);
330
+
331
+ const result = await llmService.isDataChannelTokenEnabled();
332
+
333
+ sinon.assert.calledOnceWithExactly(
334
+ webex.internal.feature.getFeature,
335
+ 'developer',
336
+ 'data-channel-with-jwt-token'
337
+ );
148
338
 
149
- await expect(instance.disconnectLLM({})).rejects.toThrow('Disconnect failed');
339
+ assert.equal(result, true);
150
340
  });
151
341
  });
342
+
343
+ describe('#refreshDataChannelToken', () => {
344
+ it('returns null and logs warn if no handler is set', async () => {
345
+ const warnSpy = llmService.logger.warn
346
+
347
+ const result = await llmService.refreshDataChannelToken();
348
+
349
+ assert.equal(result, null);
350
+
351
+ sinon.assert.calledOnce(warnSpy);
352
+ sinon.assert.calledWithMatch(
353
+ warnSpy,
354
+ sinon.match('LLM refreshHandler is not set')
355
+ );
356
+ });
357
+
358
+ it('returns token when handler resolves', async () => {
359
+ const mockToken = { body: { datachannelToken: 'newToken', isPracticeSession: false } };
360
+ const handler = sinon.stub().resolves(mockToken);
361
+
362
+ llmService.setRefreshHandler(handler);
363
+
364
+ const token = await llmService.refreshDataChannelToken();
365
+
366
+ assert.equal(token, mockToken);
367
+ sinon.assert.calledOnce(handler);
368
+ });
369
+
370
+ it('logs warn and returns null when handler rejects', async () => {
371
+ const handler = sinon.stub().rejects(new Error('throw error'));
372
+ llmService.setRefreshHandler(handler);
373
+
374
+ const warnSpy = llmService.logger.warn
375
+
376
+ const result = await llmService.refreshDataChannelToken();
377
+
378
+ assert.equal(result, null);
379
+
380
+ sinon.assert.calledOnce(warnSpy);
381
+ sinon.assert.calledWithMatch(
382
+ warnSpy,
383
+ sinon.match('DataChannel token refresh failed'),
384
+ );
385
+ });
386
+ });
387
+
388
+ describe('#getDatachannelToken / #setDatachannelToken', () => {
389
+ it('sets and gets datachannel token', () => {
390
+ llmService.setDatachannelToken('abc123','llm-default-session');
391
+ assert.equal(llmService.getDatachannelToken('llm-default-session'), 'abc123');
392
+ llmService.setDatachannelToken('123abc','llm-practice-session');
393
+ assert.equal(llmService.getDatachannelToken('llm-practice-session'), '123abc');
394
+ });
395
+ });
396
+
397
+ describe('multi-connection logic', () => {
398
+ const locusUrl2 = 'locusUrl2';
399
+ const datachannelUrl2 = 'datachannelUrl2';
400
+
401
+ it('tracks multiple sessions independently', async () => {
402
+ await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
403
+ await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
404
+
405
+ assert.equal(llmService.isConnected('s1'), true);
406
+ assert.equal(llmService.isConnected('s2'), true);
407
+ assert.equal(llmService.getLocusUrl('s1'), locusUrl);
408
+ assert.equal(llmService.getLocusUrl('s2'), locusUrl2);
409
+ assert.equal(llmService.getDatachannelUrl('s1'), datachannelUrl);
410
+ assert.equal(llmService.getDatachannelUrl('s2'), datachannelUrl2);
411
+
412
+ const all = llmService.getAllConnections();
413
+ assert.equal(all.has('s1'), true);
414
+ assert.equal(all.has('s2'), true);
415
+ });
416
+
417
+ it('disconnectLLM clears only the targeted session', async () => {
418
+ llmService.disconnect = sinon.stub().resolves(true);
419
+
420
+ await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
421
+ await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
422
+
423
+ const options = {code: 1000, reason: 'test'};
424
+ await llmService.disconnectLLM(options, 's1');
425
+
426
+ sinon.assert.calledOnceWithExactly(llmService.disconnect, options, 's1');
427
+
428
+ const all = llmService.getAllConnections();
429
+ assert.equal(all.has('s1'), false);
430
+ assert.equal(all.has('s2'), true);
431
+
432
+ assert.equal(llmService.datachannelTokens['s1'], undefined);
433
+ });
434
+
435
+ it('disconnectAllLLM clears all sessions', async () => {
436
+ llmService.disconnectAll = sinon.stub().resolves(true);
437
+ sinon.spy(llmService, 'resetDatachannelTokens');
438
+
439
+ await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
440
+ await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
441
+
442
+ await llmService.disconnectAllLLM({code: 1000, reason: 'all'});
443
+
444
+ sinon.assert.calledOnce(llmService.disconnectAll);
445
+ assert.equal(llmService.getAllConnections().size, 0);
446
+ });
447
+ });
448
+
152
449
  });
153
450
  });