@webex/internal-plugin-llm 3.11.0 → 3.12.0-llmrefactor.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.
@@ -2,152 +2,540 @@ import MockWebex from '@webex/test-helper-mock-webex';
2
2
  import {assert} from '@webex/test-helper-chai';
3
3
  import sinon from 'sinon';
4
4
  import Mercury from '@webex/internal-plugin-mercury';
5
- import LLMService from '@webex/internal-plugin-llm';
5
+ import LLMChannel from '@webex/internal-plugin-llm/src/llm';
6
6
 
7
7
  describe('plugin-llm', () => {
8
8
  const locusUrl = 'locusUrl';
9
9
  const datachannelUrl = 'datachannelUrl';
10
10
 
11
- describe('llm', () => {
12
- let webex, llmService;
11
+ describe('LLMChannel', () => {
12
+ let webex, llmChannel;
13
13
 
14
14
  beforeEach(() => {
15
15
  webex = new MockWebex({
16
16
  children: {
17
17
  mercury: Mercury,
18
- llm: LLMService,
18
+ llm: LLMChannel,
19
19
  },
20
20
  });
21
21
 
22
- llmService = webex.internal.llm;
23
- llmService.connect = sinon.stub().callsFake(() => {
24
- llmService.connected = true;
25
- });
26
- llmService.disconnect = sinon.stub().resolves(true);
27
- llmService.request = sinon.stub().resolves({
22
+ webex.internal.feature = {
23
+ setFeature: sinon.stub().resolves({value: true}),
24
+ getFeature: sinon.stub().resolves(true),
25
+ };
26
+
27
+ llmChannel = webex.internal.llm;
28
+ // Stub Mercury's prototype disconnect so super.disconnect() works in tests
29
+ sinon.stub(Object.getPrototypeOf(Object.getPrototypeOf(llmChannel)), 'disconnect').resolves();
30
+ llmChannel.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
+ llmChannel.connect = sinon.stub().callsFake(() => {
38
+ llmChannel.connected = true;
39
+ });
34
40
  });
35
41
 
42
+ afterEach(() => sinon.restore());
43
+
36
44
  describe('#registerAndConnect', () => {
37
- it('registers connection', async () => {
38
- llmService.register = sinon.stub().resolves({
39
- body: {
40
- binding: 'binding',
41
- webSocketUrl: 'url',
42
- },
45
+ it('registers and connects', async () => {
46
+ assert.equal(llmChannel.isConnected(), false);
47
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
48
+ assert.equal(llmChannel.isConnected(), true);
49
+ assert.equal(llmChannel.getLocusUrl(), locusUrl);
50
+ assert.equal(llmChannel.getDatachannelUrl(), datachannelUrl);
51
+ });
52
+
53
+ it('connects with subscriptionAwareSubchannels when token enabled', async () => {
54
+ llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
55
+
56
+ const buildSpy = sinon.spy(LLMChannel, 'buildUrlWithAwareSubchannels');
57
+
58
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl, 'abc123');
59
+
60
+ sinon.assert.calledOnce(buildSpy);
61
+ sinon.assert.calledOnce(llmChannel.connect);
62
+
63
+ const calledUrl = llmChannel.connect.getCall(0).args[0];
64
+ assert.include(calledUrl, 'subscriptionAwareSubchannels=');
65
+ });
66
+
67
+ it('connects without subscriptionAwareSubchannels when token disabled', async () => {
68
+ llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(false);
69
+
70
+ const buildSpy = sinon.spy(LLMChannel, 'buildUrlWithAwareSubchannels');
71
+
72
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
73
+
74
+ sinon.assert.notCalled(buildSpy);
75
+ sinon.assert.calledOnce(llmChannel.connect);
76
+
77
+ const calledUrl = llmChannel.connect.getCall(0).args[0];
78
+ assert.equal(calledUrl, 'wss://example.com/socket');
79
+ });
80
+
81
+ it('connects with subscriptionAwareSubchannels when token enabled but token missing', async () => {
82
+ llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
83
+
84
+ const buildSpy = sinon.spy(LLMChannel, 'buildUrlWithAwareSubchannels');
85
+
86
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl, undefined);
87
+
88
+ sinon.assert.calledOnce(buildSpy);
89
+ sinon.assert.calledOnce(llmChannel.connect);
90
+
91
+ const calledUrl = llmChannel.connect.getCall(0).args[0];
92
+ assert.include(calledUrl, 'subscriptionAwareSubchannels=');
93
+
94
+ buildSpy.restore();
95
+ });
96
+
97
+ it('deduplicates concurrent calls', async () => {
98
+ const promise1 = llmChannel.registerAndConnect(locusUrl, datachannelUrl);
99
+ const promise2 = llmChannel.registerAndConnect(locusUrl, datachannelUrl);
100
+
101
+ assert.strictEqual(promise1, promise2);
102
+ await promise1;
103
+ });
104
+
105
+ it('avoids reconnect if already connected to same URLs', async () => {
106
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
107
+ sinon.assert.calledOnce(llmChannel.connect);
108
+
109
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
110
+ sinon.assert.calledOnce(llmChannel.connect);
111
+ });
112
+
113
+ it('propagates error when registration request fails', async () => {
114
+ const error = new Error('Network failure');
115
+ llmChannel.request = sinon.stub().rejects(error);
116
+
117
+ try {
118
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
119
+ assert.fail('should have thrown');
120
+ } catch (e) {
121
+ assert.equal(e.message, 'Network failure');
122
+ }
123
+
124
+ sinon.assert.notCalled(llmChannel.connect);
125
+ });
126
+
127
+ it('returns timing data on successful connection', async () => {
128
+ const clock = sinon.useFakeTimers();
129
+
130
+ llmChannel.request = sinon.stub().callsFake(async () => {
131
+ clock.tick(37);
132
+
133
+ return {
134
+ headers: {},
135
+ body: {
136
+ binding: 'binding',
137
+ webSocketUrl: 'wss://example.com/socket',
138
+ },
139
+ };
140
+ });
141
+ llmChannel.connect = sinon.stub().callsFake(async () => {
142
+ clock.tick(23);
143
+ llmChannel.connected = true;
144
+ });
145
+
146
+ const result = await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
147
+
148
+ assert.deepEqual(result, {
149
+ clientLLMDatachannelResponseTime: 37,
150
+ clientLLMWebSocketConnectTime: 23,
151
+ });
152
+
153
+ clock.restore();
154
+ });
155
+
156
+ it('attaches timing to error when connect fails', async () => {
157
+ const clock = sinon.useFakeTimers();
158
+
159
+ llmChannel.request = sinon.stub().callsFake(async () => {
160
+ clock.tick(37);
161
+
162
+ return {
163
+ headers: {},
164
+ body: {
165
+ binding: 'binding',
166
+ webSocketUrl: 'wss://example.com/socket',
167
+ },
168
+ };
43
169
  });
44
- assert.equal(llmService.isConnected(), false);
45
- await llmService.registerAndConnect(locusUrl, datachannelUrl);
46
- assert.equal(llmService.isConnected(), true);
170
+
171
+ const connectError = new Error('websocket connect failed');
172
+ llmChannel.connect = sinon.stub().rejects(connectError);
173
+
174
+ let caughtError;
175
+ try {
176
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
177
+ } catch (error) {
178
+ caughtError = error;
179
+ }
180
+
181
+ assert.equal(caughtError, connectError);
182
+ assert.deepEqual(caughtError.timing, {clientLLMDatachannelResponseTime: 37});
183
+
184
+ clock.restore();
185
+ });
186
+
187
+ it('clears connectingPromise after successful connection', async () => {
188
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
189
+
190
+ // After completion, a new call should create a fresh promise (not deduplicated)
191
+ llmChannel.connect.resetHistory();
192
+ llmChannel.request.resetHistory();
193
+
194
+ // Simulate different URLs to force new connection
195
+ await llmChannel.registerAndConnect('newLocusUrl', 'newDatachannelUrl');
196
+
197
+ // Should have called request again since connectingPromise was cleared
198
+ sinon.assert.calledOnce(llmChannel.request);
47
199
  });
48
200
 
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
- },
201
+ it('clears connectingPromise after failed connection', async () => {
202
+ llmChannel.connect = sinon.stub().rejects(new Error('connect failed'));
203
+
204
+ try {
205
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
206
+ } catch (e) {
207
+ // expected
208
+ }
209
+
210
+ // Reset and try again - should not deduplicate since promise was cleared
211
+ llmChannel.connect = sinon.stub().callsFake(() => {
212
+ llmChannel.connected = true;
55
213
  });
56
- await llmService.registerAndConnect();
57
- assert.equal(llmService.isConnected(), false);
214
+ llmChannel.request.resetHistory();
215
+
216
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
217
+
218
+ // Should have called request again
219
+ sinon.assert.calledOnce(llmChannel.request);
220
+ });
221
+
222
+ it('returns undefined when already connected to same URLs', async () => {
223
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
224
+
225
+ const result = await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
226
+
227
+ assert.equal(result, undefined);
58
228
  });
59
229
  });
60
230
 
61
- describe('#register', () => {
62
- it('registers connection', async () => {
63
- await llmService.register(datachannelUrl);
231
+ describe('#register (via registerAndConnect)', () => {
232
+ it('sends request with token header when token enabled and provided', async () => {
233
+ llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
234
+
235
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl, 'abc123');
236
+
237
+ sinon.assert.calledOnceWithExactly(
238
+ llmChannel.request,
239
+ sinon.match({
240
+ method: 'POST',
241
+ url: datachannelUrl,
242
+ body: {deviceUrl: webex.internal.device.url},
243
+ headers: {'Data-Channel-Auth-Token': 'abc123'},
244
+ })
245
+ );
246
+ });
247
+
248
+ it('sends request without token header when none provided', async () => {
249
+ llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
250
+
251
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
64
252
 
65
253
  sinon.assert.calledOnceWithExactly(
66
- llmService.request,
254
+ llmChannel.request,
67
255
  sinon.match({
68
256
  method: 'POST',
69
- url: `${datachannelUrl}`,
257
+ url: datachannelUrl,
70
258
  body: {deviceUrl: webex.internal.device.url},
259
+ headers: {},
71
260
  })
72
261
  );
262
+ });
263
+
264
+ it('sends request without token header when toggle disabled', async () => {
265
+ llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(false);
73
266
 
74
- assert.equal(llmService.getBinding(), 'binding');
267
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl, 'abc123');
268
+
269
+ sinon.assert.calledOnceWithExactly(
270
+ llmChannel.request,
271
+ sinon.match({
272
+ method: 'POST',
273
+ url: datachannelUrl,
274
+ body: {deviceUrl: webex.internal.device.url},
275
+ headers: {},
276
+ })
277
+ );
75
278
  });
76
279
  });
77
280
 
78
281
  describe('#getLocusUrl', () => {
79
282
  it('gets LocusUrl', async () => {
80
- llmService.register = sinon.stub().resolves({
81
- body: {
82
- binding: 'binding',
83
- webSocketUrl: 'url',
84
- },
85
- });
86
- await llmService.registerAndConnect(locusUrl, datachannelUrl);
87
- assert.equal(llmService.getLocusUrl(), locusUrl);
283
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
284
+ assert.equal(llmChannel.getLocusUrl(), locusUrl);
285
+ });
286
+
287
+ it('returns undefined before connection', () => {
288
+ assert.equal(llmChannel.getLocusUrl(), undefined);
88
289
  });
89
290
  });
90
291
 
91
292
  describe('#getDatachannelUrl', () => {
92
293
  it('gets dataChannel Url', async () => {
93
- llmService.register = sinon.stub().resolves({
94
- body: {
95
- binding: 'binding',
96
- webSocketUrl: 'url',
97
- },
98
- });
99
- await llmService.registerAndConnect(locusUrl, datachannelUrl);
100
- assert.equal(llmService.getDatachannelUrl(), datachannelUrl);
294
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
295
+ assert.equal(llmChannel.getDatachannelUrl(), datachannelUrl);
296
+ });
297
+
298
+ it('returns undefined before connection', () => {
299
+ assert.equal(llmChannel.getDatachannelUrl(), undefined);
300
+ });
301
+ });
302
+
303
+ describe('#getBinding', () => {
304
+ it('gets binding after connection', async () => {
305
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
306
+ assert.equal(llmChannel.getBinding(), 'binding');
307
+ });
308
+
309
+ it('returns undefined before connection', () => {
310
+ assert.equal(llmChannel.getBinding(), undefined);
311
+ });
312
+ });
313
+
314
+ describe('#isConnecting', () => {
315
+ it('returns true while connection is in progress', () => {
316
+ // Start connection but don't await
317
+ llmChannel.registerAndConnect(locusUrl, datachannelUrl);
318
+
319
+ assert.equal(llmChannel.isConnecting(), true);
320
+ });
321
+
322
+ it('returns false after connection completes', async () => {
323
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
324
+
325
+ assert.equal(llmChannel.isConnecting(), false);
326
+ });
327
+
328
+ it('returns false before any connection attempt', () => {
329
+ assert.equal(llmChannel.isConnecting(), false);
101
330
  });
102
331
  });
103
332
 
104
333
  describe('#disconnect', () => {
105
- it('disconnects mercury', async () => {
106
- await llmService.disconnect();
107
- sinon.assert.calledOnce(llmService.disconnect);
108
- assert.equal(llmService.isConnected(), false);
109
- assert.equal(llmService.getLocusUrl(), undefined);
110
- assert.equal(llmService.getDatachannelUrl(), undefined);
111
- assert.equal(llmService.getBinding(), undefined);
334
+ it('calls super.disconnect and clears all connection state', async () => {
335
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
336
+ llmChannel.setDatachannelToken('token123');
337
+
338
+ assert.equal(llmChannel.isConnected(), true);
339
+ assert.equal(llmChannel.getLocusUrl(), locusUrl);
340
+ assert.equal(llmChannel.getDatachannelUrl(), datachannelUrl);
341
+ assert.equal(llmChannel.getBinding(), 'binding');
342
+ assert.equal(llmChannel.getDatachannelToken(), 'token123');
343
+
344
+ await llmChannel.disconnect({code: 1000, reason: 'test'});
345
+
346
+ assert.equal(llmChannel.getLocusUrl(), undefined);
347
+ assert.equal(llmChannel.getDatachannelUrl(), undefined);
348
+ assert.equal(llmChannel.getBinding(), undefined);
349
+ assert.equal(llmChannel.getDatachannelToken(), undefined);
350
+ });
351
+
352
+ it('works without options', async () => {
353
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
354
+
355
+ await llmChannel.disconnect();
356
+
357
+ assert.equal(llmChannel.getLocusUrl(), undefined);
358
+ });
359
+
360
+ it('propagates disconnect errors', async () => {
361
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
362
+ // Change the existing stub to reject
363
+ Object.getPrototypeOf(Object.getPrototypeOf(llmChannel)).disconnect.rejects(
364
+ new Error('disconnect failed')
365
+ );
366
+
367
+ try {
368
+ await llmChannel.disconnect({code: 1000, reason: 'test'});
369
+ assert.fail('should have thrown');
370
+ } catch (error) {
371
+ assert.equal(error.message, 'disconnect failed');
372
+ }
373
+ });
374
+
375
+ it('emits disconnected event', async () => {
376
+ await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
377
+ const disconnectedSpy = sinon.spy();
378
+
379
+ llmChannel.on('disconnected', disconnectedSpy);
380
+
381
+ await llmChannel.disconnect({code: 1000, reason: 'test'});
382
+
383
+ sinon.assert.calledOnce(disconnectedSpy);
384
+ });
385
+ });
386
+
387
+ describe('#setRefreshHandler', () => {
388
+ it('stores the provided handler', async () => {
389
+ const handler = sinon.stub().resolves({body: {datachannelToken: 'newToken'}});
390
+ llmChannel.setRefreshHandler(handler);
391
+
392
+ const result = await llmChannel.refreshDataChannelToken();
393
+
394
+ assert.equal(result.body.datachannelToken, 'newToken');
395
+ sinon.assert.calledOnce(handler);
396
+ });
397
+ });
398
+
399
+ describe('#isDataChannelTokenEnabled', () => {
400
+ it('returns value from feature toggle', async () => {
401
+ webex.internal.feature.getFeature.resolves(true);
402
+
403
+ const result = await llmChannel.isDataChannelTokenEnabled();
404
+
405
+ sinon.assert.calledOnceWithExactly(
406
+ webex.internal.feature.getFeature,
407
+ 'developer',
408
+ 'data-channel-with-jwt-token'
409
+ );
410
+
411
+ assert.equal(result, true);
412
+ });
413
+ });
414
+
415
+ describe('#refreshDataChannelToken', () => {
416
+ it('returns null and logs warn if no handler is set', async () => {
417
+ const warnSpy = llmChannel.logger.warn;
418
+
419
+ const result = await llmChannel.refreshDataChannelToken();
420
+
421
+ assert.equal(result, null);
422
+
423
+ sinon.assert.calledOnce(warnSpy);
424
+ sinon.assert.calledWithMatch(warnSpy, sinon.match('LLM refreshHandler is not set'));
425
+ });
426
+
427
+ it('returns token when handler resolves', async () => {
428
+ const mockToken = {body: {datachannelToken: 'newToken', datachannelTokenType: 'default'}};
429
+ const handler = sinon.stub().resolves(mockToken);
430
+
431
+ llmChannel.setRefreshHandler(handler);
432
+
433
+ const token = await llmChannel.refreshDataChannelToken();
434
+
435
+ assert.equal(token, mockToken);
436
+ sinon.assert.calledOnce(handler);
437
+ });
438
+
439
+ it('logs warn and returns null when handler rejects', async () => {
440
+ const handler = sinon.stub().rejects(new Error('throw error'));
441
+ llmChannel.setRefreshHandler(handler);
442
+
443
+ const warnSpy = llmChannel.logger.warn;
444
+
445
+ const result = await llmChannel.refreshDataChannelToken();
446
+
447
+ assert.equal(result, null);
448
+
449
+ sinon.assert.calledOnce(warnSpy);
450
+ sinon.assert.calledWithMatch(warnSpy, sinon.match('DataChannel token refresh failed'));
451
+ });
452
+ });
453
+
454
+ describe('#getDatachannelToken / #setDatachannelToken / #clearDatachannelToken', () => {
455
+ it('sets and gets datachannel token', () => {
456
+ llmChannel.setDatachannelToken('abc123');
457
+ assert.equal(llmChannel.getDatachannelToken(), 'abc123');
458
+ });
459
+
460
+ it('returns undefined when no token is set', () => {
461
+ assert.equal(llmChannel.getDatachannelToken(), undefined);
462
+ });
463
+
464
+ it('clears datachannel token', () => {
465
+ llmChannel.setDatachannelToken('abc123');
466
+ assert.equal(llmChannel.getDatachannelToken(), 'abc123');
467
+
468
+ llmChannel.clearDatachannelToken();
469
+ assert.equal(llmChannel.getDatachannelToken(), undefined);
112
470
  });
113
471
  });
114
472
 
115
- describe('disconnectLLM', () => {
116
- let instance;
473
+ describe('.matchesDatachannelRequestUrl', () => {
474
+ it('returns true when request URL starts with registration URL', () => {
475
+ const registrationUrl =
476
+ 'https://board.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
477
+ const requestUrl = registrationUrl + '/some-path';
478
+
479
+ const result = LLMChannel.matchesDatachannelRequestUrl(requestUrl, registrationUrl);
480
+
481
+ assert.equal(result, true);
482
+ });
483
+
484
+ it('returns false when URLs do not match', () => {
485
+ const registrationUrl =
486
+ 'https://board.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
487
+ const requestUrl = 'https://unknown.example.com/path';
488
+
489
+ const result = LLMChannel.matchesDatachannelRequestUrl(requestUrl, registrationUrl);
490
+
491
+ assert.equal(result, false);
492
+ });
493
+
494
+ it('returns true when request host is rewritten but pathname matches', () => {
495
+ const registrationUrl =
496
+ 'https://board-b.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
497
+ const requestUrl =
498
+ 'https://hostmap-rewritten.example.com/datachannel/api/v1/locus/ps-encoded/registrations/events';
499
+
500
+ const result = LLMChannel.matchesDatachannelRequestUrl(requestUrl, registrationUrl);
501
+
502
+ assert.equal(result, true);
503
+ });
117
504
 
118
- beforeEach(() => {
119
- instance = {
120
- 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;
131
- });
132
- }
133
- };
505
+ it('returns false for empty or null URLs', () => {
506
+ assert.equal(LLMChannel.matchesDatachannelRequestUrl('', 'https://example.com'), false);
507
+ assert.equal(LLMChannel.matchesDatachannelRequestUrl('https://example.com', ''), false);
508
+ assert.equal(LLMChannel.matchesDatachannelRequestUrl(null, 'https://example.com'), false);
509
+ assert.equal(LLMChannel.matchesDatachannelRequestUrl('https://example.com', null), false);
134
510
  });
135
511
 
136
- it('should call disconnect and clear relevant properties', async () => {
137
- await instance.disconnectLLM({});
512
+ it('returns false for invalid URLs', () => {
513
+ const result = LLMChannel.matchesDatachannelRequestUrl('not-a-url', 'also-not-a-url');
138
514
 
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();
515
+ assert.equal(result, false);
144
516
  });
517
+ });
518
+
519
+ describe('.buildUrlWithAwareSubchannels', () => {
520
+ it('adds subscriptionAwareSubchannels query param', () => {
521
+ const baseUrl = 'wss://example.com/socket';
522
+ const subchannels = ['channel1', 'channel2'];
145
523
 
146
- it('should handle errors from disconnect gracefully', async () => {
147
- instance.disconnect.mockRejectedValue(new Error('Disconnect failed'));
524
+ const result = LLMChannel.buildUrlWithAwareSubchannels(baseUrl, subchannels);
148
525
 
149
- await expect(instance.disconnectLLM({})).rejects.toThrow('Disconnect failed');
526
+ assert.include(result, 'subscriptionAwareSubchannels=channel1%2Cchannel2');
527
+ });
528
+
529
+ it('preserves existing query params', () => {
530
+ const baseUrl = 'wss://example.com/socket?existing=param';
531
+ const subchannels = ['channel1'];
532
+
533
+ const result = LLMChannel.buildUrlWithAwareSubchannels(baseUrl, subchannels);
534
+
535
+ assert.include(result, 'existing=param');
536
+ assert.include(result, 'subscriptionAwareSubchannels=channel1');
150
537
  });
151
538
  });
539
+
152
540
  });
153
541
  });