@theia/ai-copilot 1.75.0 → 1.76.0-next.10
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.
- package/README.md +86 -14
- package/lib/browser/copilot-auth-dialog.d.ts.map +1 -1
- package/lib/browser/copilot-auth-dialog.js +12 -4
- package/lib/browser/copilot-auth-dialog.js.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts +15 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.js +52 -24
- package/lib/browser/copilot-frontend-application-contribution.js.map +1 -1
- package/lib/common/copilot-auth-service.d.ts +27 -24
- package/lib/common/copilot-auth-service.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.d.ts +3 -21
- package/lib/common/copilot-language-models-manager.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.js +1 -13
- package/lib/common/copilot-language-models-manager.js.map +1 -1
- package/lib/common/copilot-preferences.d.ts +1 -0
- package/lib/common/copilot-preferences.d.ts.map +1 -1
- package/lib/common/copilot-preferences.js +24 -6
- package/lib/common/copilot-preferences.js.map +1 -1
- package/lib/common/index.d.ts +0 -1
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +0 -1
- package/lib/common/index.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.d.ts +25 -16
- package/lib/node/copilot-auth-service-impl.d.ts.map +1 -1
- package/lib/node/copilot-auth-service-impl.js +67 -183
- package/lib/node/copilot-auth-service-impl.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.spec.js +104 -168
- package/lib/node/copilot-auth-service-impl.spec.js.map +1 -1
- package/lib/node/copilot-backend-module.d.ts.map +1 -1
- package/lib/node/copilot-backend-module.js +10 -2
- package/lib/node/copilot-backend-module.js.map +1 -1
- package/lib/node/copilot-cli-auth-provider.d.ts +146 -0
- package/lib/node/copilot-cli-auth-provider.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.js +430 -0
- package/lib/node/copilot-cli-auth-provider.js.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts +2 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.js +249 -0
- package/lib/node/copilot-cli-auth-provider.spec.js.map +1 -0
- package/lib/node/copilot-cli-locator.d.ts +120 -0
- package/lib/node/copilot-cli-locator.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.js +348 -0
- package/lib/node/copilot-cli-locator.js.map +1 -0
- package/lib/node/copilot-cli-locator.spec.d.ts +2 -0
- package/lib/node/copilot-cli-locator.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.spec.js +327 -0
- package/lib/node/copilot-cli-locator.spec.js.map +1 -0
- package/lib/node/copilot-credential-store.d.ts +45 -0
- package/lib/node/copilot-credential-store.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.js +96 -0
- package/lib/node/copilot-credential-store.js.map +1 -0
- package/lib/node/copilot-credential-store.spec.d.ts +2 -0
- package/lib/node/copilot-credential-store.spec.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.spec.js +93 -0
- package/lib/node/copilot-credential-store.spec.js.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.d.ts +20 -18
- package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.js +39 -67
- package/lib/node/copilot-language-models-manager-impl.js.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts +2 -0
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js +119 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js.map +1 -0
- package/lib/node/copilot-runtime-env.d.ts +20 -0
- package/lib/node/copilot-runtime-env.d.ts.map +1 -0
- package/lib/node/copilot-runtime-env.js +49 -0
- package/lib/node/copilot-runtime-env.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.d.ts +103 -0
- package/lib/node/copilot-sdk-client-provider.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.js +247 -0
- package/lib/node/copilot-sdk-client-provider.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.js +215 -0
- package/lib/node/copilot-sdk-client-provider.spec.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.d.ts +85 -0
- package/lib/node/copilot-sdk-language-model.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.js +301 -0
- package/lib/node/copilot-sdk-language-model.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.js +302 -0
- package/lib/node/copilot-sdk-language-model.spec.js.map +1 -0
- package/lib/node/copilot-sdk-loader.d.ts +57 -0
- package/lib/node/copilot-sdk-loader.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.js +133 -0
- package/lib/node/copilot-sdk-loader.js.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.js +113 -0
- package/lib/node/copilot-sdk-loader.spec.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.d.ts +49 -0
- package/lib/node/copilot-sdk-mappers.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.js +147 -0
- package/lib/node/copilot-sdk-mappers.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.js +133 -0
- package/lib/node/copilot-sdk-mappers.spec.js.map +1 -0
- package/lib/node/copilot-sdk-types.d.ts +198 -0
- package/lib/node/copilot-sdk-types.d.ts.map +1 -0
- package/lib/{common/copilot-oauth-config.js → node/copilot-sdk-types.js} +2 -13
- package/lib/node/copilot-sdk-types.js.map +1 -0
- package/lib/node/index.d.ts +9 -1
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +9 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +4 -6
- package/src/browser/copilot-auth-dialog.tsx +12 -8
- package/src/browser/copilot-frontend-application-contribution.ts +62 -29
- package/src/common/copilot-auth-service.ts +28 -24
- package/src/common/copilot-language-models-manager.ts +3 -28
- package/src/common/copilot-preferences.ts +24 -5
- package/src/common/index.ts +0 -1
- package/src/node/copilot-auth-service-impl.spec.ts +122 -207
- package/src/node/copilot-auth-service-impl.ts +63 -218
- package/src/node/copilot-backend-module.ts +11 -2
- package/src/node/copilot-cli-auth-provider.spec.ts +315 -0
- package/src/node/copilot-cli-auth-provider.ts +444 -0
- package/src/node/copilot-cli-locator.spec.ts +386 -0
- package/src/node/copilot-cli-locator.ts +363 -0
- package/src/node/copilot-credential-store.spec.ts +106 -0
- package/src/node/copilot-credential-store.ts +99 -0
- package/src/node/copilot-language-models-manager-impl.spec.ts +137 -0
- package/src/node/copilot-language-models-manager-impl.ts +51 -85
- package/src/node/copilot-runtime-env.ts +46 -0
- package/src/node/copilot-sdk-client-provider.spec.ts +253 -0
- package/src/node/copilot-sdk-client-provider.ts +241 -0
- package/src/node/copilot-sdk-language-model.spec.ts +363 -0
- package/src/node/copilot-sdk-language-model.ts +329 -0
- package/src/node/copilot-sdk-loader.spec.ts +126 -0
- package/src/node/copilot-sdk-loader.ts +142 -0
- package/src/node/copilot-sdk-mappers.spec.ts +151 -0
- package/src/node/copilot-sdk-mappers.ts +161 -0
- package/src/node/copilot-sdk-types.ts +276 -0
- package/src/node/index.ts +9 -1
- package/lib/common/copilot-oauth-config.d.ts +0 -33
- package/lib/common/copilot-oauth-config.d.ts.map +0 -1
- package/lib/common/copilot-oauth-config.js.map +0 -1
- package/lib/node/copilot-language-model.d.ts +0 -34
- package/lib/node/copilot-language-model.d.ts.map +0 -1
- package/lib/node/copilot-language-model.js +0 -236
- package/lib/node/copilot-language-model.js.map +0 -1
- package/lib/node/copilot-language-model.spec.d.ts +0 -2
- package/lib/node/copilot-language-model.spec.d.ts.map +0 -1
- package/lib/node/copilot-language-model.spec.js +0 -89
- package/lib/node/copilot-language-model.spec.js.map +0 -1
- package/src/common/copilot-oauth-config.ts +0 -55
- package/src/node/copilot-language-model.spec.ts +0 -106
- package/src/node/copilot-language-model.ts +0 -273
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { LanguageModel, LanguageModelStatus } from '@theia/ai-core';
|
|
19
|
+
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
|
|
20
|
+
import { CopilotAuthState } from '../common/copilot-auth-service';
|
|
21
|
+
import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
|
|
22
|
+
|
|
23
|
+
/** A registry that records the models and the patches it received. */
|
|
24
|
+
class FakeRegistry {
|
|
25
|
+
|
|
26
|
+
readonly models: LanguageModel[] = [];
|
|
27
|
+
readonly patches: Array<{ id: string, patch: Partial<LanguageModel> }> = [];
|
|
28
|
+
|
|
29
|
+
async getLanguageModel(id: string): Promise<LanguageModel | undefined> {
|
|
30
|
+
return this.models.find(model => model.id === id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getLanguageModels(): Promise<LanguageModel[]> {
|
|
34
|
+
return this.models;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addLanguageModels(models: LanguageModel[]): void {
|
|
38
|
+
this.models.push(...models);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
removeLanguageModels(ids: string[]): void {
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const index = this.models.findIndex(model => model.id === id);
|
|
44
|
+
if (index >= 0) {
|
|
45
|
+
this.models.splice(index, 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async patchLanguageModel(id: string, patch: Partial<LanguageModel>): Promise<void> {
|
|
51
|
+
this.patches.push({ id, patch });
|
|
52
|
+
Object.assign(this.models.find(model => model.id === id) ?? {}, patch);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class TestableCopilotLanguageModelsManagerImpl extends CopilotLanguageModelsManagerImpl {
|
|
57
|
+
|
|
58
|
+
authState: CopilotAuthState = { isAuthenticated: true };
|
|
59
|
+
listFailure: Error | undefined;
|
|
60
|
+
modelIds: string[] = ['gpt-5'];
|
|
61
|
+
|
|
62
|
+
constructor(readonly registry: FakeRegistry) {
|
|
63
|
+
super();
|
|
64
|
+
Object.assign(this, {
|
|
65
|
+
logger: new MockLogger(),
|
|
66
|
+
languageModelRegistry: registry,
|
|
67
|
+
authService: {
|
|
68
|
+
getAuthState: async () => this.authState,
|
|
69
|
+
onAuthStateChanged: () => ({ dispose: () => { } })
|
|
70
|
+
},
|
|
71
|
+
sdkClientProvider: {
|
|
72
|
+
listModelIds: async () => {
|
|
73
|
+
if (this.listFailure) {
|
|
74
|
+
throw this.listFailure;
|
|
75
|
+
}
|
|
76
|
+
return this.modelIds;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
callCalculateStatus(): Promise<LanguageModelStatus> {
|
|
83
|
+
return this.calculateStatus();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('CopilotLanguageModelsManagerImpl - status', () => {
|
|
88
|
+
|
|
89
|
+
let manager: TestableCopilotLanguageModelsManagerImpl;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
manager = new TestableCopilotLanguageModelsManagerImpl(new FakeRegistry());
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should report a model as ready for a user who is signed in', async () => {
|
|
96
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
97
|
+
expect(manager.registry.models[0].status).to.deep.equal({ status: 'ready' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should report a model as unavailable while nobody is signed in', async () => {
|
|
101
|
+
manager.authState = { isAuthenticated: false };
|
|
102
|
+
expect(await manager.callCalculateStatus()).to.deep.include({ status: 'unavailable' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should report models as unavailable when they cannot be listed, rather than as ready', async () => {
|
|
106
|
+
manager.listFailure = new Error('not authorized to use this Copilot feature');
|
|
107
|
+
expect(await manager.fetchAvailableModelIds()).to.be.empty;
|
|
108
|
+
const status = await manager.callCalculateStatus();
|
|
109
|
+
expect(status.status).to.equal('unavailable');
|
|
110
|
+
expect(status.message).to.contain('not authorized');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should reflect a failed listing in the models that are already registered', async () => {
|
|
114
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
115
|
+
manager.listFailure = new Error('socket hang up');
|
|
116
|
+
await manager.fetchAvailableModelIds();
|
|
117
|
+
expect(manager.registry.patches.map(patch => patch.id)).to.deep.equal(['copilot/gpt-5']);
|
|
118
|
+
expect(manager.registry.models[0].status).to.deep.include({ status: 'unavailable' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should report ready again once the models can be listed', async () => {
|
|
122
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
123
|
+
manager.listFailure = new Error('socket hang up');
|
|
124
|
+
await manager.fetchAvailableModelIds();
|
|
125
|
+
manager.listFailure = undefined;
|
|
126
|
+
expect(await manager.fetchAvailableModelIds()).to.deep.equal(['gpt-5']);
|
|
127
|
+
expect(manager.registry.models[0].status).to.deep.equal({ status: 'ready' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should update an existing model instead of registering it twice', async () => {
|
|
131
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
132
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 5 });
|
|
133
|
+
expect(manager.registry.models).to.have.lengthOf(1);
|
|
134
|
+
expect(manager.registry.patches).to.have.lengthOf(1);
|
|
135
|
+
expect(manager.registry.patches[0].patch).to.deep.include({ maxRetries: 5 });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { LanguageModelRegistry, LanguageModelStatus } from '@theia/ai-core';
|
|
18
|
-
import {
|
|
19
|
-
import { inject, injectable, postConstruct,
|
|
20
|
-
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
18
|
+
import { DisposableCollection, ILogger, nls } from '@theia/core';
|
|
19
|
+
import { inject, injectable, named, postConstruct, preDestroy } from '@theia/core/shared/inversify';
|
|
20
|
+
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID } from '../common';
|
|
21
|
+
import { CopilotSdkLanguageModel } from './copilot-sdk-language-model';
|
|
22
|
+
import { CopilotSdkClientProvider } from './copilot-sdk-client-provider';
|
|
23
23
|
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -27,7 +27,7 @@ import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
|
27
27
|
* Manages registration and lifecycle of Copilot language models in the AI language model registry.
|
|
28
28
|
*/
|
|
29
29
|
@injectable()
|
|
30
|
-
export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsManager
|
|
30
|
+
export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsManager {
|
|
31
31
|
|
|
32
32
|
@inject(LanguageModelRegistry)
|
|
33
33
|
protected readonly languageModelRegistry: LanguageModelRegistry;
|
|
@@ -35,36 +35,51 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
35
35
|
@inject(CopilotAuthServiceImpl)
|
|
36
36
|
protected readonly authService: CopilotAuthServiceImpl;
|
|
37
37
|
|
|
38
|
-
@inject(
|
|
39
|
-
protected readonly
|
|
38
|
+
@inject(CopilotSdkClientProvider)
|
|
39
|
+
protected readonly sdkClientProvider: CopilotSdkClientProvider;
|
|
40
40
|
|
|
41
41
|
@inject(ILogger) @named('ai-copilot:CopilotLanguageModelsManagerImpl')
|
|
42
42
|
protected readonly logger: ILogger;
|
|
43
43
|
|
|
44
|
-
protected enterpriseUrl: string | undefined;
|
|
45
44
|
protected readonly toDispose = new DisposableCollection();
|
|
46
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Why the last model discovery failed, if it did.
|
|
48
|
+
*
|
|
49
|
+
* Being signed in does not imply that requests are accepted: the credentials can lack the Copilot
|
|
50
|
+
* entitlement, be revoked, or belong to a subscription served by a different API host. Reporting
|
|
51
|
+
* the models as ready in those cases claims a working setup that is not there, so the failure is
|
|
52
|
+
* remembered and reflected in the status of the models.
|
|
53
|
+
*/
|
|
54
|
+
protected discoveryFailure: string | undefined;
|
|
55
|
+
|
|
47
56
|
@postConstruct()
|
|
48
57
|
protected init(): void {
|
|
49
58
|
this.toDispose.push(this.authService.onAuthStateChanged(() => {
|
|
50
|
-
|
|
59
|
+
// The listener cannot be awaited, so the failure is reported here rather than escaping as
|
|
60
|
+
// an unhandled rejection.
|
|
61
|
+
this.refreshModelsStatus().catch(error => this.logger.warn('Copilot: failed to update the status of the models:', error));
|
|
51
62
|
}));
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Detaches from the auth service when the container this manager lives in goes away.
|
|
67
|
+
* Unbinding a container runs `@preDestroy` rather than `dispose`, see the client provider.
|
|
68
|
+
*/
|
|
69
|
+
@preDestroy()
|
|
70
|
+
protected stop(): void {
|
|
55
71
|
this.toDispose.dispose();
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
setEnterpriseUrl(url: string | undefined): void {
|
|
59
|
-
this.enterpriseUrl = url;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
74
|
protected async calculateStatus(): Promise<LanguageModelStatus> {
|
|
63
75
|
const authState = await this.authService.getAuthState();
|
|
64
|
-
if (authState.isAuthenticated) {
|
|
65
|
-
return { status: '
|
|
76
|
+
if (!authState.isAuthenticated) {
|
|
77
|
+
return { status: 'unavailable', message: nls.localize('theia/ai/copilot/notSignedIn', 'Not signed in to GitHub Copilot') };
|
|
78
|
+
}
|
|
79
|
+
if (this.discoveryFailure) {
|
|
80
|
+
return { status: 'unavailable', message: this.discoveryFailure };
|
|
66
81
|
}
|
|
67
|
-
return { status: '
|
|
82
|
+
return { status: 'ready' };
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
async createOrUpdateLanguageModels(...modelDescriptions: CopilotModelDescription[]): Promise<void> {
|
|
@@ -74,29 +89,24 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
74
89
|
const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id);
|
|
75
90
|
|
|
76
91
|
if (model) {
|
|
77
|
-
if (!(model instanceof
|
|
92
|
+
if (!(model instanceof CopilotSdkLanguageModel)) {
|
|
78
93
|
this.logger.warn(`Copilot: model ${modelDescription.id} is not a Copilot model`);
|
|
79
94
|
continue;
|
|
80
95
|
}
|
|
81
|
-
await this.languageModelRegistry.patchLanguageModel<
|
|
96
|
+
await this.languageModelRegistry.patchLanguageModel<CopilotSdkLanguageModel>(modelDescription.id, {
|
|
82
97
|
model: modelDescription.model,
|
|
83
|
-
enableStreaming: modelDescription.enableStreaming,
|
|
84
|
-
supportsStructuredOutput: modelDescription.supportsStructuredOutput,
|
|
85
98
|
status,
|
|
86
99
|
maxRetries: modelDescription.maxRetries
|
|
87
100
|
});
|
|
88
101
|
} else {
|
|
89
102
|
this.languageModelRegistry.addLanguageModels([
|
|
90
|
-
new
|
|
103
|
+
new CopilotSdkLanguageModel(
|
|
91
104
|
modelDescription.id,
|
|
92
105
|
modelDescription.model,
|
|
93
106
|
status,
|
|
94
|
-
modelDescription.enableStreaming,
|
|
95
|
-
modelDescription.supportsStructuredOutput,
|
|
96
107
|
modelDescription.maxRetries,
|
|
97
|
-
() => this.
|
|
98
|
-
|
|
99
|
-
() => this.oauthConfig.userAgent
|
|
108
|
+
() => this.sdkClientProvider.getClient(),
|
|
109
|
+
this.logger
|
|
100
110
|
)
|
|
101
111
|
]);
|
|
102
112
|
}
|
|
@@ -112,8 +122,8 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
112
122
|
const allModels = await this.languageModelRegistry.getLanguageModels();
|
|
113
123
|
|
|
114
124
|
for (const model of allModels) {
|
|
115
|
-
if (model instanceof
|
|
116
|
-
await this.languageModelRegistry.patchLanguageModel<
|
|
125
|
+
if (model instanceof CopilotSdkLanguageModel && model.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
|
|
126
|
+
await this.languageModelRegistry.patchLanguageModel<CopilotSdkLanguageModel>(model.id, {
|
|
117
127
|
status
|
|
118
128
|
});
|
|
119
129
|
}
|
|
@@ -121,70 +131,26 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
async fetchAvailableModelIds(): Promise<string[]> {
|
|
124
|
-
const accessToken = await this.authService.getAccessToken();
|
|
125
|
-
if (!accessToken) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const baseURL = getCopilotApiBaseUrl(this.enterpriseUrl);
|
|
130
|
-
|
|
131
134
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'User-Agent': this.oauthConfig.userAgent,
|
|
136
|
-
'Accept': 'application/json'
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!response.ok) {
|
|
141
|
-
this.logger.warn(`Copilot: failed to fetch available models: ${response.status} ${response.statusText}`);
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const data = await response.json() as {
|
|
146
|
-
data?: Array<{ id: string; capabilities?: { family?: string } }>
|
|
147
|
-
};
|
|
148
|
-
const models = data.data ?? [];
|
|
149
|
-
const modelIds = this.deduplicateModels(models);
|
|
150
|
-
this.logger.info(`Copilot: discovered ${modelIds.length} available models: ${modelIds.join(', ')}`);
|
|
135
|
+
const modelIds = await this.sdkClientProvider.listModelIds();
|
|
136
|
+
this.logger.info(`Copilot: discovered ${modelIds.length} models [${modelIds.join(', ')}]`);
|
|
137
|
+
await this.setDiscoveryFailure(undefined);
|
|
151
138
|
return modelIds;
|
|
152
139
|
} catch (error) {
|
|
153
|
-
this.logger.warn('Copilot: failed to fetch available models:', error);
|
|
140
|
+
this.logger.warn('Copilot: failed to fetch available models via the Copilot CLI:', error);
|
|
141
|
+
await this.setDiscoveryFailure(error instanceof Error ? error.message : String(error));
|
|
154
142
|
return [];
|
|
155
143
|
}
|
|
156
144
|
}
|
|
157
145
|
|
|
158
146
|
/**
|
|
159
|
-
*
|
|
160
|
-
* The API returns both alias IDs (e.g., `gpt-4o`) and versioned IDs
|
|
161
|
-
* (e.g., `gpt-4o-2024-11-20`) for the same model family.
|
|
162
|
-
* We keep only the family alias when it exists, falling back to
|
|
163
|
-
* the versioned ID otherwise.
|
|
147
|
+
* Records the outcome of the last model discovery and reflects it in the status of the models.
|
|
164
148
|
*/
|
|
165
|
-
protected
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const seenFamilies = new Set<string>();
|
|
169
|
-
|
|
170
|
-
for (const model of models) {
|
|
171
|
-
const family = model.capabilities?.family;
|
|
172
|
-
if (family && seenFamilies.has(family)) {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (family) {
|
|
176
|
-
seenFamilies.add(family);
|
|
177
|
-
// Prefer the family alias if it exists as a model ID
|
|
178
|
-
if (allIds.has(family)) {
|
|
179
|
-
result.push(family);
|
|
180
|
-
} else {
|
|
181
|
-
result.push(model.id);
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
result.push(model.id);
|
|
185
|
-
}
|
|
149
|
+
protected async setDiscoveryFailure(failure: string | undefined): Promise<void> {
|
|
150
|
+
if (this.discoveryFailure === failure) {
|
|
151
|
+
return;
|
|
186
152
|
}
|
|
187
|
-
|
|
188
|
-
|
|
153
|
+
this.discoveryFailure = failure;
|
|
154
|
+
await this.refreshModelsStatus();
|
|
189
155
|
}
|
|
190
156
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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
|
+
/**
|
|
18
|
+
* The environment of the processes this integration starts, which are the Copilot CLI of a sign-in
|
|
19
|
+
* and the one that serves the requests.
|
|
20
|
+
*/
|
|
21
|
+
export namespace CopilotRuntimeEnv {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The variables the Copilot CLI accepts a token from. None of them is a sign-in of this
|
|
25
|
+
* application, so a token this application does not own must not reach the CLI through them.
|
|
26
|
+
*/
|
|
27
|
+
export const TOKEN_VARIABLES = ['COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The given environment without the variables that carry a token.
|
|
31
|
+
*
|
|
32
|
+
* They are dropped by name and regardless of their case, rather than masked with `undefined`.
|
|
33
|
+
* Environment variables are case-insensitive on Windows, so a `Github_Token` of the backend would
|
|
34
|
+
* survive a `GITHUB_TOKEN: undefined` next to it and would still be read by the CLI.
|
|
35
|
+
*/
|
|
36
|
+
export function withoutTokens(env: NodeJS.ProcessEnv): Record<string, string | undefined> {
|
|
37
|
+
const dropped = new Set(TOKEN_VARIABLES.map(name => name.toLowerCase()));
|
|
38
|
+
const result: Record<string, string | undefined> = {};
|
|
39
|
+
for (const [name, value] of Object.entries(env)) {
|
|
40
|
+
if (!dropped.has(name.toLowerCase())) {
|
|
41
|
+
result[name] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { promises as fs } from 'fs';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
|
|
22
|
+
import { FileUri } from '@theia/core/lib/common/file-uri';
|
|
23
|
+
import { CopilotCredentials } from './copilot-credential-store';
|
|
24
|
+
import { CopilotSdkClientProvider } from './copilot-sdk-client-provider';
|
|
25
|
+
import type { CopilotClient, CopilotClientOptions } from './copilot-sdk-types';
|
|
26
|
+
|
|
27
|
+
interface SdkStub {
|
|
28
|
+
api: {
|
|
29
|
+
CopilotClient: new (options?: CopilotClientOptions) => CopilotClient,
|
|
30
|
+
RuntimeConnection: { forStdio: (options: { path: string }) => unknown }
|
|
31
|
+
};
|
|
32
|
+
started: string[];
|
|
33
|
+
stopped: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The SDK as the provider uses it: a client constructor and the stdio connection factory. */
|
|
37
|
+
function sdkStub(): SdkStub {
|
|
38
|
+
const started: string[] = [];
|
|
39
|
+
const stopped: string[] = [];
|
|
40
|
+
class StubClient {
|
|
41
|
+
constructor(readonly options?: CopilotClientOptions) { }
|
|
42
|
+
async start(): Promise<void> {
|
|
43
|
+
started.push(String(this.options?.gitHubToken));
|
|
44
|
+
}
|
|
45
|
+
async stop(): Promise<Error[]> {
|
|
46
|
+
stopped.push(String(this.options?.gitHubToken));
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
async listModels(): Promise<[]> {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
api: {
|
|
55
|
+
CopilotClient: StubClient as unknown as new (options?: CopilotClientOptions) => CopilotClient,
|
|
56
|
+
RuntimeConnection: { forStdio: (options: { path: string }) => ({ kind: 'stdio', ...options }) }
|
|
57
|
+
},
|
|
58
|
+
started,
|
|
59
|
+
stopped
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class TestableCopilotSdkClientProvider extends CopilotSdkClientProvider {
|
|
64
|
+
|
|
65
|
+
credentials: CopilotCredentials | undefined = { token: 'gho_first' };
|
|
66
|
+
|
|
67
|
+
constructor(sdk: SdkStub['api'], configDirectory: string, cli = join('/opt', 'copilot', 'copilot')) {
|
|
68
|
+
super();
|
|
69
|
+
Object.assign(this, {
|
|
70
|
+
logger: new MockLogger(),
|
|
71
|
+
credentialStore: { get: async () => this.credentials },
|
|
72
|
+
cliLocator: { resolve: async () => cli },
|
|
73
|
+
sdkLoader: { load: async () => sdk },
|
|
74
|
+
envVariablesServer: { getConfigDirUri: async () => FileUri.create(configDirectory).toString() }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
callCreateRuntimeEnv(credentials: CopilotCredentials): Record<string, string | undefined> {
|
|
79
|
+
return this.createRuntimeEnv(credentials);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
callEnrichEntitlementError(error: unknown): unknown {
|
|
83
|
+
return this.enrichEntitlementError(error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
clientOptions(client: CopilotClient): CopilotClientOptions | undefined {
|
|
87
|
+
return (client as unknown as { options?: CopilotClientOptions }).options;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('CopilotSdkClientProvider - client options', () => {
|
|
92
|
+
|
|
93
|
+
let configDirectory: string;
|
|
94
|
+
let sdk: ReturnType<typeof sdkStub>;
|
|
95
|
+
|
|
96
|
+
beforeEach(async () => {
|
|
97
|
+
configDirectory = await fs.mkdtemp(join(os.tmpdir(), 'theia-copilot-config-'));
|
|
98
|
+
sdk = sdkStub();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(async () => {
|
|
102
|
+
await fs.rm(configDirectory, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should hand the located CLI and the token of this application to the runtime', async () => {
|
|
106
|
+
const provider = new TestableCopilotSdkClientProvider(sdk.api, configDirectory);
|
|
107
|
+
const options = provider.clientOptions(await provider.getClient());
|
|
108
|
+
expect(options?.connection).to.deep.equal({ kind: 'stdio', path: join('/opt', 'copilot', 'copilot') });
|
|
109
|
+
expect(options?.gitHubToken).to.equal('gho_first');
|
|
110
|
+
// Nothing the runtime finds on the host may make it appear signed in.
|
|
111
|
+
expect(options?.useLoggedInUser).to.be.false;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should keep the runtime out of the ambient behaviour of the CLI', async () => {
|
|
115
|
+
const provider = new TestableCopilotSdkClientProvider(sdk.api, configDirectory);
|
|
116
|
+
const options = provider.clientOptions(await provider.getClient());
|
|
117
|
+
expect(options?.mode).to.equal('empty');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should point the runtime at a Copilot home of the application and create it', async () => {
|
|
121
|
+
const provider = new TestableCopilotSdkClientProvider(sdk.api, configDirectory);
|
|
122
|
+
const options = provider.clientOptions(await provider.getClient());
|
|
123
|
+
const home = join(configDirectory, 'copilot');
|
|
124
|
+
// Compared without case, because the URI of the configuration directory lowercases the drive
|
|
125
|
+
// letter on Windows, which addresses the same location.
|
|
126
|
+
expect(options?.baseDirectory?.toLowerCase()).to.equal(home.toLowerCase());
|
|
127
|
+
expect((await fs.stat(home)).isDirectory()).to.be.true;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('CopilotSdkClientProvider - runtime environment', () => {
|
|
132
|
+
|
|
133
|
+
const provider = new TestableCopilotSdkClientProvider(sdkStub().api, os.tmpdir());
|
|
134
|
+
|
|
135
|
+
it('should remove tokens of the environment, which are not a sign-in of this application', () => {
|
|
136
|
+
const previous = { ...process.env };
|
|
137
|
+
try {
|
|
138
|
+
process.env.GITHUB_TOKEN = 'gho_environment';
|
|
139
|
+
process.env.GH_TOKEN = 'gho_environment';
|
|
140
|
+
process.env.COPILOT_GITHUB_TOKEN = 'gho_environment';
|
|
141
|
+
const env = provider.callCreateRuntimeEnv({ token: 'gho_ours' });
|
|
142
|
+
expect(env.GITHUB_TOKEN).to.be.undefined;
|
|
143
|
+
expect(env.GH_TOKEN).to.be.undefined;
|
|
144
|
+
expect(env.COPILOT_GITHUB_TOKEN).to.be.undefined;
|
|
145
|
+
} finally {
|
|
146
|
+
process.env = previous;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should remove a token variable whatever its case, since Windows ignores it', () => {
|
|
151
|
+
const previous = { ...process.env };
|
|
152
|
+
try {
|
|
153
|
+
process.env.Gh_Token = 'gho_environment';
|
|
154
|
+
const env = provider.callCreateRuntimeEnv({ token: 'gho_ours' });
|
|
155
|
+
expect(Object.keys(env).some(name => name.toLowerCase() === 'gh_token')).to.be.false;
|
|
156
|
+
} finally {
|
|
157
|
+
process.env = previous;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should point the runtime at the deployment that was signed in to', () => {
|
|
162
|
+
const env = provider.callCreateRuntimeEnv({ token: 'gho_ours', enterpriseUrl: 'company.ghe.com' });
|
|
163
|
+
expect(env.COPILOT_GH_HOST).to.equal('company.ghe.com');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should leave the host alone for a sign-in against GitHub.com', () => {
|
|
167
|
+
expect(provider.callCreateRuntimeEnv({ token: 'gho_ours' })).to.not.have.property('COPILOT_GH_HOST');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('CopilotSdkClientProvider - lifecycle', () => {
|
|
172
|
+
|
|
173
|
+
let configDirectory: string;
|
|
174
|
+
let sdk: ReturnType<typeof sdkStub>;
|
|
175
|
+
let provider: TestableCopilotSdkClientProvider;
|
|
176
|
+
|
|
177
|
+
beforeEach(async () => {
|
|
178
|
+
configDirectory = await fs.mkdtemp(join(os.tmpdir(), 'theia-copilot-config-'));
|
|
179
|
+
sdk = sdkStub();
|
|
180
|
+
provider = new TestableCopilotSdkClientProvider(sdk.api, configDirectory);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(async () => {
|
|
184
|
+
await fs.rm(configDirectory, { recursive: true, force: true });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should reject when the application is not signed in', async () => {
|
|
188
|
+
provider.credentials = undefined;
|
|
189
|
+
try {
|
|
190
|
+
await provider.getClient();
|
|
191
|
+
expect.fail('should have rejected');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
expect(String(error)).to.contain('sign in');
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should start one CLI and share it, also for concurrent callers', async () => {
|
|
198
|
+
const [first, second] = await Promise.all([provider.getClient(), provider.getClient()]);
|
|
199
|
+
expect(first).to.equal(second);
|
|
200
|
+
expect(await provider.getClient()).to.equal(first);
|
|
201
|
+
expect(sdk.started).to.deep.equal(['gho_first']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should replace the client when the credentials changed, stopping the previous one', async () => {
|
|
205
|
+
const first = await provider.getClient();
|
|
206
|
+
provider.credentials = { token: 'gho_second' };
|
|
207
|
+
const second = await provider.getClient();
|
|
208
|
+
expect(second).to.not.equal(first);
|
|
209
|
+
expect(sdk.started).to.deep.equal(['gho_first', 'gho_second']);
|
|
210
|
+
expect(sdk.stopped).to.deep.equal(['gho_first']);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should stop the CLI on reset and start a new one afterwards', async () => {
|
|
214
|
+
await provider.getClient();
|
|
215
|
+
await provider.reset();
|
|
216
|
+
expect(sdk.stopped).to.deep.equal(['gho_first']);
|
|
217
|
+
await provider.getClient();
|
|
218
|
+
expect(sdk.started).to.deep.equal(['gho_first', 'gho_first']);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should not cache a client whose runtime failed to start', async () => {
|
|
222
|
+
const failing = sdkStub();
|
|
223
|
+
failing.api.CopilotClient = class {
|
|
224
|
+
async start(): Promise<void> {
|
|
225
|
+
throw new Error('runtime did not come up');
|
|
226
|
+
}
|
|
227
|
+
} as unknown as new () => CopilotClient;
|
|
228
|
+
const other = new TestableCopilotSdkClientProvider(failing.api, configDirectory);
|
|
229
|
+
await other.getClient().catch(() => { /* expected */ });
|
|
230
|
+
// A cached rejection would keep replaying instead of trying again.
|
|
231
|
+
await other.getClient().catch(error => expect(String(error)).to.contain('did not come up'));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('CopilotSdkClientProvider - entitlement errors', () => {
|
|
236
|
+
|
|
237
|
+
const provider = new TestableCopilotSdkClientProvider(sdkStub().api, os.tmpdir());
|
|
238
|
+
|
|
239
|
+
it('should explain what an authorization failure of the runtime can mean', () => {
|
|
240
|
+
const enriched = provider.callEnrichEntitlementError(new Error('user is not authorized to use this Copilot feature'));
|
|
241
|
+
expect(String(enriched)).to.contain('subscription');
|
|
242
|
+
expect(String(enriched)).to.contain('not authorized to use this Copilot feature');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should explain a policy rejection the same way', () => {
|
|
246
|
+
expect(String(provider.callEnrichEntitlementError(new Error('Access denied by policy settings')))).to.contain('administrator');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should pass an unrelated failure through unchanged', () => {
|
|
250
|
+
const error = new Error('socket hang up');
|
|
251
|
+
expect(provider.callEnrichEntitlementError(error)).to.equal(error);
|
|
252
|
+
});
|
|
253
|
+
});
|