Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/test/browser/modelPickerDelegate.test.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { Emitter, Event } from '../../../../../base/common/event.js';
8
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
9
import { observableValue } from '../../../../../base/common/observable.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
12
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
13
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
14
import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';
15
import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';
16
import { CLAUDE_CODE_SESSION_TYPE, COPILOT_CLI_SESSION_TYPE } from '../../../../services/sessions/common/session.js';
17
import { IActiveSession, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';
18
import { ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js';
19
import { getAvailableModels, modelPickerStorageKey, SessionModelPicker } from '../../browser/copilotChatSessionsActions.js';
20
21
function makeModel(id: string, sessionType: string): ILanguageModelChatMetadataAndIdentifier {
22
return {
23
identifier: id,
24
metadata: { targetChatSessionType: sessionType } as ILanguageModelChatMetadata,
25
};
26
}
27
28
function stubServices(
29
disposables: DisposableStore,
30
opts?: {
31
models?: ILanguageModelChatMetadataAndIdentifier[];
32
activeSession?: Partial<IActiveSession>;
33
storedEntries?: Map<string, string>;
34
setModelSpy?: (sessionId: string, modelId: string) => void;
35
},
36
): { instantiationService: TestInstantiationService; storage: Map<string, string>; activeSession: ReturnType<typeof observableValue<IActiveSession | undefined>>; fireLanguageModelsChanged: () => void } {
37
const instantiationService = disposables.add(new TestInstantiationService());
38
const models = opts?.models ?? [];
39
const storage = opts?.storedEntries ?? new Map<string, string>();
40
41
const activeSession = opts?.activeSession
42
? observableValue<IActiveSession | undefined>('activeSession', opts.activeSession as IActiveSession)
43
: observableValue<IActiveSession | undefined>('activeSession', undefined);
44
45
const setModelSpy = opts?.setModelSpy ?? (() => { });
46
47
const onDidChangeLanguageModelsEmitter = disposables.add(new Emitter<{ added?: readonly { identifier: string }[]; removed?: readonly string[] }>());
48
49
instantiationService.stub(ILanguageModelsService, {
50
onDidChangeLanguageModels: onDidChangeLanguageModelsEmitter.event,
51
getLanguageModelIds: () => models.map(m => m.identifier),
52
lookupLanguageModel: (id: string) => models.find(m => m.identifier === id)?.metadata,
53
} as Partial<ILanguageModelsService>);
54
55
instantiationService.stub(IStorageService, {
56
get: (key: string, _scope: StorageScope) => storage.get(key),
57
store: (key: string, value: string, _scope: StorageScope, _target: StorageTarget) => { storage.set(key, value); },
58
} as Partial<IStorageService>);
59
60
const provider: Partial<ISessionsProvider> = {
61
id: 'default-copilot',
62
setModel: setModelSpy,
63
};
64
65
instantiationService.stub(ISessionsManagementService, {
66
activeSession,
67
} as unknown as ISessionsManagementService);
68
69
instantiationService.stub(ISessionsProvidersService, {
70
onDidChangeProviders: Event.None,
71
getProviders: () => [provider as ISessionsProvider],
72
} as Partial<ISessionsProvidersService>);
73
74
// Stub IInstantiationService so SessionModelPicker can call createInstance for ModelPickerActionItem
75
instantiationService.stub(IInstantiationService, instantiationService);
76
77
return { instantiationService, storage, activeSession, fireLanguageModelsChanged: () => onDidChangeLanguageModelsEmitter.fire({}) };
78
}
79
80
suite('modelPickerStorageKey', () => {
81
ensureNoDisposablesAreLeakedInTestSuite();
82
83
test('produces per-session-type keys', () => {
84
assert.strictEqual(modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE), `sessions.modelPicker.${COPILOT_CLI_SESSION_TYPE}.selectedModelId`);
85
assert.strictEqual(modelPickerStorageKey(CLAUDE_CODE_SESSION_TYPE), `sessions.modelPicker.${CLAUDE_CODE_SESSION_TYPE}.selectedModelId`);
86
});
87
});
88
89
suite('getAvailableModels', () => {
90
const disposables = new DisposableStore();
91
92
teardown(() => disposables.clear());
93
ensureNoDisposablesAreLeakedInTestSuite();
94
95
test('returns empty when no active session', () => {
96
const models = [makeModel('model-1', COPILOT_CLI_SESSION_TYPE)];
97
const { instantiationService } = stubServices(disposables, { models });
98
const languageModelsService = instantiationService.get(ILanguageModelsService);
99
const sessionsManagementService = instantiationService.get(ISessionsManagementService);
100
const result = getAvailableModels(languageModelsService, sessionsManagementService);
101
assert.deepStrictEqual(result, []);
102
});
103
104
test('filters models by session type', () => {
105
const models = [
106
makeModel('cli-model', COPILOT_CLI_SESSION_TYPE),
107
makeModel('cloud-model', 'copilot-cloud'),
108
makeModel('claude-model', CLAUDE_CODE_SESSION_TYPE),
109
];
110
const { instantiationService } = stubServices(disposables, {
111
models,
112
activeSession: { providerId: 'default-copilot', sessionId: 'sess-1', sessionType: CLAUDE_CODE_SESSION_TYPE },
113
});
114
const languageModelsService = instantiationService.get(ILanguageModelsService);
115
const sessionsManagementService = instantiationService.get(ISessionsManagementService);
116
const result = getAvailableModels(languageModelsService, sessionsManagementService);
117
assert.deepStrictEqual(result, [models[2]]);
118
});
119
});
120
121
suite('SessionModelPicker', () => {
122
const disposables = new DisposableStore();
123
124
teardown(() => disposables.clear());
125
ensureNoDisposablesAreLeakedInTestSuite();
126
127
test('stores selected model under session-type-scoped key', () => {
128
const models = [makeModel('model-1', CLAUDE_CODE_SESSION_TYPE)];
129
const { instantiationService, storage } = stubServices(disposables, {
130
models,
131
activeSession: { providerId: 'default-copilot', sessionId: 'sess-1', sessionType: CLAUDE_CODE_SESSION_TYPE },
132
});
133
// Creating the picker triggers initModel which calls setModel for the first available model
134
disposables.add(instantiationService.createInstance(SessionModelPicker));
135
assert.strictEqual(storage.get(modelPickerStorageKey(CLAUDE_CODE_SESSION_TYPE)), 'model-1');
136
assert.strictEqual(storage.has(modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE)), false);
137
});
138
139
test('calls provider.setModel on init', () => {
140
const calls: { sessionId: string; modelId: string }[] = [];
141
const models = [makeModel('model-1', CLAUDE_CODE_SESSION_TYPE)];
142
const { instantiationService } = stubServices(disposables, {
143
models,
144
activeSession: { providerId: 'default-copilot', sessionId: 'sess-1', sessionType: CLAUDE_CODE_SESSION_TYPE },
145
setModelSpy: (sessionId, modelId) => calls.push({ sessionId, modelId }),
146
});
147
disposables.add(instantiationService.createInstance(SessionModelPicker));
148
assert.ok(calls.some(c => c.sessionId === 'sess-1' && c.modelId === 'model-1'));
149
});
150
151
test('remembers model per session type from storage', () => {
152
const models = [makeModel('model-a', CLAUDE_CODE_SESSION_TYPE), makeModel('model-b', CLAUDE_CODE_SESSION_TYPE)];
153
const storedEntries = new Map([[modelPickerStorageKey(CLAUDE_CODE_SESSION_TYPE), 'model-b']]);
154
const calls: { sessionId: string; modelId: string }[] = [];
155
const { instantiationService } = stubServices(disposables, {
156
models,
157
activeSession: { providerId: 'default-copilot', sessionId: 'sess-1', sessionType: CLAUDE_CODE_SESSION_TYPE },
158
storedEntries,
159
setModelSpy: (sessionId, modelId) => calls.push({ sessionId, modelId }),
160
});
161
disposables.add(instantiationService.createInstance(SessionModelPicker));
162
// Should pick model-b (remembered) instead of model-a (first)
163
assert.ok(calls.some(c => c.modelId === 'model-b'));
164
});
165
166
test('does not throw when no active session', () => {
167
const { instantiationService } = stubServices(disposables);
168
assert.doesNotThrow(() => disposables.add(instantiationService.createInstance(SessionModelPicker)));
169
});
170
171
test('different session types use independent storage keys', () => {
172
const cliModels = [makeModel('cli-m', COPILOT_CLI_SESSION_TYPE)];
173
const claudeModels = [makeModel('claude-m', CLAUDE_CODE_SESSION_TYPE)];
174
const allModels = [...cliModels, ...claudeModels];
175
176
const { instantiationService, storage, activeSession } = stubServices(disposables, {
177
models: allModels,
178
activeSession: { providerId: 'default-copilot', sessionId: 's1', sessionType: COPILOT_CLI_SESSION_TYPE },
179
});
180
disposables.add(instantiationService.createInstance(SessionModelPicker));
181
assert.strictEqual(storage.get(modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE)), 'cli-m');
182
183
// Switch session type
184
activeSession.set({ providerId: 'default-copilot', sessionId: 's2', sessionType: CLAUDE_CODE_SESSION_TYPE } as IActiveSession, undefined);
185
186
assert.strictEqual(storage.get(modelPickerStorageKey(CLAUDE_CODE_SESSION_TYPE)), 'claude-m');
187
// CLI key should still be intact
188
assert.strictEqual(storage.get(modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE)), 'cli-m');
189
});
190
191
test('propagates selected model to a new session of the same type (#313385)', () => {
192
const models = [makeModel('cli-a', COPILOT_CLI_SESSION_TYPE), makeModel('cli-b', COPILOT_CLI_SESSION_TYPE)];
193
const storedEntries = new Map([[modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE), 'cli-b']]);
194
const calls: { sessionId: string; modelId: string }[] = [];
195
const { instantiationService, activeSession } = stubServices(disposables, {
196
models,
197
activeSession: { providerId: 'default-copilot', sessionId: 's1', sessionType: COPILOT_CLI_SESSION_TYPE },
198
storedEntries,
199
setModelSpy: (sessionId, modelId) => calls.push({ sessionId, modelId }),
200
});
201
disposables.add(instantiationService.createInstance(SessionModelPicker));
202
// Initial session receives the remembered model.
203
assert.ok(calls.some(c => c.sessionId === 's1' && c.modelId === 'cli-b'));
204
205
// Switch to a new session of the same type (e.g. user picked a different repo).
206
activeSession.set({ providerId: 'default-copilot', sessionId: 's2', sessionType: COPILOT_CLI_SESSION_TYPE } as IActiveSession, undefined);
207
208
// The new session must receive the same model so the request isn't sent with the default.
209
assert.ok(calls.some(c => c.sessionId === 's2' && c.modelId === 'cli-b'));
210
});
211
212
test('does not re-push model to the same session when language models change', () => {
213
const models = [makeModel('cli-a', COPILOT_CLI_SESSION_TYPE)];
214
const calls: { sessionId: string; modelId: string }[] = [];
215
const { instantiationService, fireLanguageModelsChanged } = stubServices(disposables, {
216
models,
217
activeSession: { providerId: 'default-copilot', sessionId: 's1', sessionType: COPILOT_CLI_SESSION_TYPE },
218
setModelSpy: (sessionId, modelId) => calls.push({ sessionId, modelId }),
219
});
220
disposables.add(instantiationService.createInstance(SessionModelPicker));
221
const initialCallCount = calls.filter(c => c.sessionId === 's1').length;
222
assert.ok(initialCallCount > 0, 'expected initial setModel to fire');
223
224
// Re-fire language-models-changed multiple times. The active session and
225
// selected model haven't changed, so the provider must not be re-notified.
226
fireLanguageModelsChanged();
227
fireLanguageModelsChanged();
228
229
assert.strictEqual(calls.filter(c => c.sessionId === 's1').length, initialCallCount);
230
});
231
});
232
233