@theia/ai-external-api 1.76.0-next.33

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 (72) hide show
  1. package/README.md +83 -0
  2. package/lib/browser/ai-external-api-frontend-contribution.d.ts +28 -0
  3. package/lib/browser/ai-external-api-frontend-contribution.d.ts.map +1 -0
  4. package/lib/browser/ai-external-api-frontend-contribution.js +88 -0
  5. package/lib/browser/ai-external-api-frontend-contribution.js.map +1 -0
  6. package/lib/browser/ai-external-api-frontend-contribution.spec.d.ts +2 -0
  7. package/lib/browser/ai-external-api-frontend-contribution.spec.d.ts.map +1 -0
  8. package/lib/browser/ai-external-api-frontend-contribution.spec.js +102 -0
  9. package/lib/browser/ai-external-api-frontend-contribution.spec.js.map +1 -0
  10. package/lib/browser/ai-external-api-frontend-module.d.ts +4 -0
  11. package/lib/browser/ai-external-api-frontend-module.d.ts.map +1 -0
  12. package/lib/browser/ai-external-api-frontend-module.js +47 -0
  13. package/lib/browser/ai-external-api-frontend-module.js.map +1 -0
  14. package/lib/browser/frontend-external-chat-session-provider.d.ts +63 -0
  15. package/lib/browser/frontend-external-chat-session-provider.d.ts.map +1 -0
  16. package/lib/browser/frontend-external-chat-session-provider.js +271 -0
  17. package/lib/browser/frontend-external-chat-session-provider.js.map +1 -0
  18. package/lib/browser/frontend-external-chat-session-provider.spec.d.ts +2 -0
  19. package/lib/browser/frontend-external-chat-session-provider.spec.d.ts.map +1 -0
  20. package/lib/browser/frontend-external-chat-session-provider.spec.js +421 -0
  21. package/lib/browser/frontend-external-chat-session-provider.spec.js.map +1 -0
  22. package/lib/browser/index.d.ts +3 -0
  23. package/lib/browser/index.d.ts.map +1 -0
  24. package/lib/browser/index.js +21 -0
  25. package/lib/browser/index.js.map +1 -0
  26. package/lib/common/external-chat-session-provider.d.ts +213 -0
  27. package/lib/common/external-chat-session-provider.d.ts.map +1 -0
  28. package/lib/common/external-chat-session-provider.js +147 -0
  29. package/lib/common/external-chat-session-provider.js.map +1 -0
  30. package/lib/common/index.d.ts +2 -0
  31. package/lib/common/index.d.ts.map +1 -0
  32. package/lib/common/index.js +20 -0
  33. package/lib/common/index.js.map +1 -0
  34. package/lib/node/ai-external-api-backend-module.d.ts +4 -0
  35. package/lib/node/ai-external-api-backend-module.d.ts.map +1 -0
  36. package/lib/node/ai-external-api-backend-module.js +38 -0
  37. package/lib/node/ai-external-api-backend-module.js.map +1 -0
  38. package/lib/node/ai-external-api-endpoint.d.ts +31 -0
  39. package/lib/node/ai-external-api-endpoint.d.ts.map +1 -0
  40. package/lib/node/ai-external-api-endpoint.js +189 -0
  41. package/lib/node/ai-external-api-endpoint.js.map +1 -0
  42. package/lib/node/ai-external-api-endpoint.spec.d.ts +2 -0
  43. package/lib/node/ai-external-api-endpoint.spec.d.ts.map +1 -0
  44. package/lib/node/ai-external-api-endpoint.spec.js +233 -0
  45. package/lib/node/ai-external-api-endpoint.spec.js.map +1 -0
  46. package/lib/node/external-chat-session-registry.d.ts +83 -0
  47. package/lib/node/external-chat-session-registry.d.ts.map +1 -0
  48. package/lib/node/external-chat-session-registry.js +266 -0
  49. package/lib/node/external-chat-session-registry.js.map +1 -0
  50. package/lib/node/external-chat-session-registry.spec.d.ts +2 -0
  51. package/lib/node/external-chat-session-registry.spec.d.ts.map +1 -0
  52. package/lib/node/external-chat-session-registry.spec.js +366 -0
  53. package/lib/node/external-chat-session-registry.spec.js.map +1 -0
  54. package/lib/node/index.d.ts +3 -0
  55. package/lib/node/index.d.ts.map +1 -0
  56. package/lib/node/index.js +21 -0
  57. package/lib/node/index.js.map +1 -0
  58. package/package.json +52 -0
  59. package/src/browser/ai-external-api-frontend-contribution.spec.ts +128 -0
  60. package/src/browser/ai-external-api-frontend-contribution.ts +90 -0
  61. package/src/browser/ai-external-api-frontend-module.ts +48 -0
  62. package/src/browser/frontend-external-chat-session-provider.spec.ts +491 -0
  63. package/src/browser/frontend-external-chat-session-provider.ts +277 -0
  64. package/src/browser/index.ts +18 -0
  65. package/src/common/external-chat-session-provider.ts +330 -0
  66. package/src/common/index.ts +17 -0
  67. package/src/node/ai-external-api-backend-module.ts +41 -0
  68. package/src/node/ai-external-api-endpoint.spec.ts +272 -0
  69. package/src/node/ai-external-api-endpoint.ts +195 -0
  70. package/src/node/external-chat-session-registry.spec.ts +413 -0
  71. package/src/node/external-chat-session-registry.ts +270 -0
  72. package/src/node/index.ts +18 -0
@@ -0,0 +1,413 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { ChatSessionStatus } from '@theia/ai-chat/lib/common/chat-model';
18
+ import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
19
+ import { expect } from 'chai';
20
+ import { ExternalApiTestSupport } from '@theia/external-api/lib/node/test/external-api-test-support';
21
+ import { ExternalChatSessionDetail, ExternalChatSessionProvider, ExternalChatSessionSummary } from '../common/external-chat-session-provider';
22
+ import { ExternalChatSessionRegistry } from './external-chat-session-registry';
23
+
24
+ describe('ExternalChatSessionRegistry', () => {
25
+
26
+ function createRegistry(queryTimeout?: number): ExternalChatSessionRegistry {
27
+ return ExternalApiTestSupport.inject(new ExternalChatSessionRegistry(), {
28
+ logger: new MockLogger(),
29
+ ...queryTimeout === undefined ? {} : { queryTimeout }
30
+ });
31
+ }
32
+
33
+ /** A provider whose queries never answer, standing in for an unresponsive frontend. */
34
+ function unresponsiveProvider(): ExternalChatSessionProvider {
35
+ const hang = (): Promise<never> => new Promise<never>(() => { });
36
+ return {
37
+ getWorkspace: hang,
38
+ getSessions: hang,
39
+ getSessionSummary: hang,
40
+ getSession: hang,
41
+ openSession: hang,
42
+ restoreSession: hang,
43
+ sendPrompt: hang,
44
+ createSession: hang
45
+ };
46
+ }
47
+
48
+ function summary(id: string, status: ChatSessionStatus = 'idle', lastInteraction?: number, restored: boolean = true): ExternalChatSessionSummary {
49
+ return { id, status, lastInteraction, restored };
50
+ }
51
+
52
+ function detail(id: string, status: ChatSessionStatus = 'idle', lastInteraction?: number, restored: boolean = true): ExternalChatSessionDetail {
53
+ return { ...summary(id, status, lastInteraction, restored), messages: [{ actor: 'user', text: `question for ${id}` }] };
54
+ }
55
+
56
+ function provider(
57
+ sessions: ExternalChatSessionSummary[],
58
+ details: ExternalChatSessionDetail[] = [],
59
+ actions: Partial<ExternalChatSessionProvider> = {}
60
+ ): ExternalChatSessionProvider {
61
+ return {
62
+ getWorkspace: async () => undefined,
63
+ getSessions: async () => sessions,
64
+ getSessionSummary: async sessionId => details.find(candidate => candidate.id === sessionId) ?? sessions.find(candidate => candidate.id === sessionId),
65
+ getSession: async sessionId => details.find(candidate => candidate.id === sessionId),
66
+ openSession: async () => false,
67
+ restoreSession: async () => undefined,
68
+ sendPrompt: async () => undefined,
69
+ createSession: async () => ({ created: { session: summary('created') } }),
70
+ ...actions
71
+ };
72
+ }
73
+
74
+ function failingProvider(): ExternalChatSessionProvider {
75
+ const fail = async (): Promise<never> => { throw new Error('connection lost'); };
76
+ return {
77
+ getWorkspace: fail,
78
+ getSessions: fail,
79
+ getSessionSummary: fail,
80
+ getSession: fail,
81
+ openSession: fail,
82
+ restoreSession: fail,
83
+ sendPrompt: fail,
84
+ createSession: fail
85
+ };
86
+ }
87
+
88
+ describe('getSessions', () => {
89
+ it('returns an empty list without providers', async () => {
90
+ const registry = createRegistry();
91
+ expect(await registry.getSessions()).to.deep.equal([]);
92
+ });
93
+
94
+ it('aggregates sessions of all providers sorted by last interaction', async () => {
95
+ const registry = createRegistry();
96
+ registry.addProvider(provider([summary('1', 'idle', 100)]));
97
+ registry.addProvider(provider([summary('2', 'idle', 300), summary('3', 'idle', 200)]));
98
+ const sessions = await registry.getSessions();
99
+ expect(sessions.map(session => session.id)).to.deep.equal(['2', '3', '1']);
100
+ });
101
+
102
+ it('deduplicates sessions preferring restored reports', async () => {
103
+ const registry = createRegistry();
104
+ registry.addProvider(provider([summary('1', 'running', 300, false)]));
105
+ registry.addProvider(provider([summary('1', 'idle', 100, true)]));
106
+ const sessions = await registry.getSessions();
107
+ expect(sessions).to.have.length(1);
108
+ expect(sessions[0].restored).to.equal(true);
109
+ });
110
+
111
+ it('deduplicates sessions preferring in-progress reports', async () => {
112
+ const registry = createRegistry();
113
+ registry.addProvider(provider([summary('1', 'idle', 200)]));
114
+ registry.addProvider(provider([summary('1', 'running', 100)]));
115
+ const sessions = await registry.getSessions();
116
+ expect(sessions).to.have.length(1);
117
+ expect(sessions[0].status).to.equal('running');
118
+ });
119
+
120
+ it('deduplicates sessions preferring more recent reports', async () => {
121
+ const registry = createRegistry();
122
+ registry.addProvider(provider([summary('1', 'idle', 100)]));
123
+ registry.addProvider(provider([summary('1', 'failed', 200)]));
124
+ const sessions = await registry.getSessions();
125
+ expect(sessions).to.have.length(1);
126
+ expect(sessions[0].status).to.equal('failed');
127
+ });
128
+
129
+ it('no longer reports sessions of removed providers', async () => {
130
+ const registry = createRegistry();
131
+ const removed = provider([summary('1')]);
132
+ registry.addProvider(removed);
133
+ registry.addProvider(provider([summary('2')]));
134
+ registry.removeProvider(removed);
135
+ const sessions = await registry.getSessions();
136
+ expect(sessions.map(session => session.id)).to.deep.equal(['2']);
137
+ });
138
+
139
+ it('skips failing providers', async () => {
140
+ const registry = createRegistry();
141
+ registry.addProvider(failingProvider());
142
+ registry.addProvider(provider([summary('1')]));
143
+ const sessions = await registry.getSessions();
144
+ expect(sessions.map(session => session.id)).to.deep.equal(['1']);
145
+ });
146
+
147
+ it('skips unresponsive providers instead of waiting for them', async () => {
148
+ const registry = createRegistry(20);
149
+ registry.addProvider(unresponsiveProvider());
150
+ registry.addProvider(provider([summary('1')]));
151
+ const sessions = await registry.getSessions();
152
+ expect(sessions.map(session => session.id)).to.deep.equal(['1']);
153
+ });
154
+ });
155
+
156
+ describe('getSession', () => {
157
+ it('returns undefined for unknown sessions', async () => {
158
+ const registry = createRegistry();
159
+ registry.addProvider(provider([]));
160
+ expect(await registry.getSession('missing')).to.equal(undefined);
161
+ });
162
+
163
+ it('returns the session from the provider that knows it', async () => {
164
+ const registry = createRegistry();
165
+ registry.addProvider(provider([]));
166
+ registry.addProvider(provider([], [detail('1', 'running')]));
167
+ const session = await registry.getSession('1');
168
+ expect(session?.status).to.equal('running');
169
+ expect(session?.messages).to.deep.equal([{ actor: 'user', text: 'question for 1' }]);
170
+ });
171
+
172
+ it('prefers the in-progress duplicate', async () => {
173
+ const registry = createRegistry();
174
+ registry.addProvider(provider([], [detail('1', 'idle', 200)]));
175
+ registry.addProvider(provider([], [detail('1', 'awaitingApproval', 100)]));
176
+ const session = await registry.getSession('1');
177
+ expect(session?.status).to.equal('awaitingApproval');
178
+ });
179
+
180
+ it('prefers the restored duplicate', async () => {
181
+ const registry = createRegistry();
182
+ registry.addProvider(provider([], [detail('1', 'idle', 100, true)]));
183
+ registry.addProvider(provider([], [detail('1', 'failed', 200, false)]));
184
+ const session = await registry.getSession('1');
185
+ expect(session?.restored).to.equal(true);
186
+ });
187
+
188
+ it('skips failing providers', async () => {
189
+ const registry = createRegistry();
190
+ registry.addProvider(failingProvider());
191
+ registry.addProvider(provider([], [detail('1')]));
192
+ const session = await registry.getSession('1');
193
+ expect(session?.id).to.equal('1');
194
+ });
195
+
196
+ it('reads the conversation from the preferred frontend only', async () => {
197
+ const registry = createRegistry();
198
+ const read: string[] = [];
199
+ const tracking = (name: string, known: ExternalChatSessionDetail): ExternalChatSessionProvider =>
200
+ provider([], [known], { getSession: async sessionId => { read.push(name); return sessionId === known.id ? known : undefined; } });
201
+ registry.addProvider(tracking('idle', detail('1', 'idle', 200)));
202
+ registry.addProvider(tracking('running', detail('1', 'running', 100)));
203
+ const session = await registry.getSession('1');
204
+ expect(session?.status).to.equal('running');
205
+ expect(read).to.deep.equal(['running']);
206
+ });
207
+ });
208
+
209
+ describe('openSession', () => {
210
+ it('returns false when no provider knows the session', async () => {
211
+ const registry = createRegistry();
212
+ registry.addProvider(provider([]));
213
+ expect(await registry.openSession('missing')).to.equal(false);
214
+ });
215
+
216
+ it('opens the session in the frontend that knows it', async () => {
217
+ const registry = createRegistry();
218
+ const opened: string[] = [];
219
+ registry.addProvider(provider([]));
220
+ registry.addProvider(provider([], [detail('1')], {
221
+ openSession: async sessionId => { opened.push(sessionId); return true; }
222
+ }));
223
+ expect(await registry.openSession('1')).to.equal(true);
224
+ expect(opened).to.deep.equal(['1']);
225
+ });
226
+
227
+ it('prefers the frontend with the restored session', async () => {
228
+ const registry = createRegistry();
229
+ const opened: string[] = [];
230
+ registry.addProvider(provider([], [detail('1', 'idle', 300, false)], {
231
+ openSession: async () => { opened.push('persisted-only'); return true; }
232
+ }));
233
+ registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
234
+ openSession: async () => { opened.push('restored'); return true; }
235
+ }));
236
+ expect(await registry.openSession('1')).to.equal(true);
237
+ expect(opened).to.deep.equal(['restored']);
238
+ });
239
+
240
+ it('falls back to the next frontend when the preferred one fails', async () => {
241
+ const registry = createRegistry();
242
+ registry.addProvider(provider([], [detail('1', 'running', 200, true)], {
243
+ openSession: async () => { throw new Error('connection lost'); }
244
+ }));
245
+ registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
246
+ openSession: async () => true
247
+ }));
248
+ expect(await registry.openSession('1')).to.equal(true);
249
+ });
250
+
251
+ it('keeps the registration order when neither report is preferred', async () => {
252
+ const registry = createRegistry();
253
+ const opened: string[] = [];
254
+ registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
255
+ openSession: async () => { opened.push('first'); return true; }
256
+ }));
257
+ registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
258
+ openSession: async () => { opened.push('second'); return true; }
259
+ }));
260
+ expect(await registry.openSession('1')).to.equal(true);
261
+ expect(opened).to.deep.equal(['first']);
262
+ });
263
+ });
264
+
265
+ describe('restoreSession', () => {
266
+ it('returns undefined when no provider knows the session', async () => {
267
+ const registry = createRegistry();
268
+ registry.addProvider(provider([]));
269
+ expect(await registry.restoreSession('missing')).to.equal(undefined);
270
+ });
271
+
272
+ it('restores the session in the frontend that knows it', async () => {
273
+ const registry = createRegistry();
274
+ const restored = detail('1', 'idle', 100, true);
275
+ registry.addProvider(provider([]));
276
+ registry.addProvider(provider([], [detail('1', 'idle', 100, false)], {
277
+ restoreSession: async () => restored
278
+ }));
279
+ expect(await registry.restoreSession('1')).to.deep.equal(restored);
280
+ });
281
+ });
282
+
283
+ describe('sendPrompt', () => {
284
+ it('returns undefined when no provider knows the session', async () => {
285
+ const registry = createRegistry();
286
+ registry.addProvider(provider([]));
287
+ expect(await registry.sendPrompt('missing', { text: 'hello' })).to.equal(undefined);
288
+ });
289
+
290
+ it('sends the prompt via the frontend that knows the session', async () => {
291
+ const registry = createRegistry();
292
+ const prompts: string[] = [];
293
+ registry.addProvider(provider([]));
294
+ registry.addProvider(provider([], [detail('1')], {
295
+ sendPrompt: async (sessionId, prompt) => { prompts.push(prompt.text); return { sent: { sessionId, requestId: 'r1' } }; }
296
+ }));
297
+ expect(await registry.sendPrompt('1', { text: 'hello' })).to.deep.equal({ sent: { sessionId: '1', requestId: 'r1' } });
298
+ expect(prompts).to.deep.equal(['hello']);
299
+ });
300
+
301
+ it('does not retry a busy rejection on other frontends', async () => {
302
+ const registry = createRegistry();
303
+ const prompted: string[] = [];
304
+ registry.addProvider(provider([], [detail('1', 'running', 100, true)], {
305
+ sendPrompt: async () => { prompted.push('restored'); return { failure: 'busy' }; }
306
+ }));
307
+ registry.addProvider(provider([], [detail('1', 'idle', 300, false)], {
308
+ sendPrompt: async sessionId => { prompted.push('persisted-only'); return { sent: { sessionId, requestId: 'r1' } }; }
309
+ }));
310
+ expect(await registry.sendPrompt('1', { text: 'hello' })).to.deep.equal({ failure: 'busy' });
311
+ expect(prompted).to.deep.equal(['restored']);
312
+ });
313
+
314
+ it('falls back to the next frontend when the preferred one fails', async () => {
315
+ const registry = createRegistry();
316
+ registry.addProvider(provider([], [detail('1', 'running', 200, true)], {
317
+ sendPrompt: async () => { throw new Error('connection lost'); }
318
+ }));
319
+ registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
320
+ sendPrompt: async sessionId => ({ sent: { sessionId, requestId: 'r1' } })
321
+ }));
322
+ expect(await registry.sendPrompt('1', { text: 'hello' })).to.deep.equal({ sent: { sessionId: '1', requestId: 'r1' } });
323
+ });
324
+ });
325
+
326
+ describe('createSession', () => {
327
+ it('rejects when no frontend is connected', async () => {
328
+ const registry = createRegistry();
329
+ expect(await registry.createSession({})).to.deep.equal({ failure: 'noFrontend' });
330
+ expect(await registry.createSession({ workspace: 'file:///project' })).to.deep.equal({ failure: 'noFrontend' });
331
+ });
332
+
333
+ it('creates in the frontend with the requested workspace', async () => {
334
+ const registry = createRegistry();
335
+ const creators: string[] = [];
336
+ registry.addProvider(provider([], [], {
337
+ getWorkspace: async () => 'file:///other',
338
+ createSession: async () => { creators.push('a'); return { created: { session: summary('a') } }; }
339
+ }));
340
+ registry.addProvider(provider([], [], {
341
+ getWorkspace: async () => 'file:///project',
342
+ createSession: async () => { creators.push('b'); return { created: { session: summary('b') } }; }
343
+ }));
344
+ const result = await registry.createSession({ workspace: 'file:///project', prompt: 'hello' });
345
+ expect(result).to.deep.equal({ created: { session: summary('b') } });
346
+ expect(creators).to.deep.equal(['b']);
347
+ });
348
+
349
+ it('matches workspaces ignoring trailing slashes', async () => {
350
+ const registry = createRegistry();
351
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project/' }));
352
+ const result = await registry.createSession({ workspace: 'file:///project' });
353
+ expect('created' in result).to.equal(true);
354
+ });
355
+
356
+ it('rejects an unknown workspace', async () => {
357
+ const registry = createRegistry();
358
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///other' }));
359
+ expect(await registry.createSession({ workspace: 'file:///project' })).to.deep.equal({ failure: 'workspaceNotFound' });
360
+ });
361
+
362
+ it('creates in the only connected frontend when no workspace is given', async () => {
363
+ const registry = createRegistry();
364
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project' }));
365
+ const result = await registry.createSession({});
366
+ expect('created' in result).to.equal(true);
367
+ });
368
+
369
+ it('rejects when no workspace is given and the frontends have different workspaces', async () => {
370
+ const registry = createRegistry();
371
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///one' }));
372
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///two' }));
373
+ expect(await registry.createSession({})).to.deep.equal({ failure: 'ambiguousWorkspace' });
374
+ });
375
+
376
+ it('creates when no workspace is given and the frontends share a workspace', async () => {
377
+ const registry = createRegistry();
378
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project' }));
379
+ registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project/' }));
380
+ const result = await registry.createSession({});
381
+ expect('created' in result).to.equal(true);
382
+ });
383
+
384
+ it('passes provider failures through', async () => {
385
+ const registry = createRegistry();
386
+ registry.addProvider(provider([], [], { createSession: async () => ({ failure: 'unknownAgent' }) }));
387
+ expect(await registry.createSession({ agentId: 'ghost' })).to.deep.equal({ failure: 'unknownAgent' });
388
+ });
389
+ });
390
+
391
+ describe('onDidChangeSessions', () => {
392
+ it('fires when providers are added, removed, or report changes', () => {
393
+ const registry = createRegistry();
394
+ let events = 0;
395
+ registry.onDidChangeSessions(() => events++);
396
+ const added = provider([]);
397
+ registry.addProvider(added);
398
+ expect(events).to.equal(1);
399
+ registry.notifySessionsChanged();
400
+ expect(events).to.equal(2);
401
+ registry.removeProvider(added);
402
+ expect(events).to.equal(3);
403
+ });
404
+
405
+ it('does not fire when removing an unknown provider', () => {
406
+ const registry = createRegistry();
407
+ let events = 0;
408
+ registry.onDidChangeSessions(() => events++);
409
+ registry.removeProvider(provider([]));
410
+ expect(events).to.equal(0);
411
+ });
412
+ });
413
+ });