Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/agentHost/test/browser/agentHostSkillButtons.test.ts
13405 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 { Codicon } from '../../../../../base/common/codicons.js';
8
import { observableValue } from '../../../../../base/common/observable.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { mock } from '../../../../../base/test/common/mock.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
12
import { isIMenuItem, isISubmenuItem, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js';
13
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
14
import { ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
15
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
16
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
17
import { IChat } from '../../../../services/sessions/common/session.js';
18
import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';
19
import { ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js';
20
import { IActiveSession, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';
21
import { AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID, IsAgentHostSession, IsAgentHostSessionContextContribution, isAgentHostSkillButtonId } from '../../browser/agentHostSkillButtons.js';
22
import { BaseAgentHostSessionsProvider } from '../../browser/baseAgentHostSessionsProvider.js';
23
// Importing this contribution registers the apply submenu on the changes toolbar,
24
// which is the slot that hosts our skill buttons as a dropdown.
25
import '../../../applyCommitsToParentRepo/browser/applyChangesToParentRepo.js';
26
27
function makeActiveSession(providerId: string): IActiveSession {
28
const chat: IChat = {
29
resource: URI.parse('file:///session'),
30
createdAt: new Date(),
31
title: observableValue('t', 'Test'),
32
updatedAt: observableValue('u', new Date()),
33
status: observableValue('s', 0),
34
changes: observableValue('c', []),
35
modelId: observableValue('m', undefined),
36
mode: observableValue('mo', undefined),
37
isArchived: observableValue('ia', false),
38
isRead: observableValue('ir', true),
39
lastTurnEnd: observableValue('lte', undefined),
40
description: observableValue('d', undefined),
41
};
42
return {
43
sessionId: `${providerId}:x`,
44
resource: chat.resource,
45
providerId,
46
sessionType: 'copilotcli',
47
icon: Codicon.copilot,
48
createdAt: chat.createdAt,
49
workspace: observableValue('w', undefined),
50
title: chat.title,
51
updatedAt: chat.updatedAt,
52
status: chat.status,
53
changes: chat.changes,
54
modelId: chat.modelId,
55
mode: chat.mode,
56
loading: observableValue('l', false),
57
isArchived: chat.isArchived,
58
isRead: chat.isRead,
59
lastTurnEnd: chat.lastTurnEnd,
60
description: chat.description,
61
gitHubInfo: observableValue('gh', undefined),
62
chats: observableValue('chats', [chat]),
63
activeChat: observableValue('ac', chat),
64
mainChat: chat,
65
capabilities: { supportsMultipleChats: false },
66
} as IActiveSession;
67
}
68
69
class FakeAgentHostProvider {
70
constructor(public readonly id: string) { }
71
}
72
// Make `instanceof BaseAgentHostSessionsProvider` return true without actually constructing one.
73
Object.setPrototypeOf(FakeAgentHostProvider.prototype, BaseAgentHostSessionsProvider.prototype);
74
75
class FakeNonAgentHostProvider {
76
constructor(public readonly id: string) { }
77
}
78
79
class FakeSessionsManagementService extends mock<ISessionsManagementService>() {
80
declare readonly _serviceBrand: undefined;
81
override readonly activeSession = observableValue<IActiveSession | undefined>('activeSession', undefined);
82
setActive(s: IActiveSession | undefined): void {
83
this.activeSession.set(s, undefined);
84
}
85
}
86
87
class FakeSessionsProvidersService extends mock<ISessionsProvidersService>() {
88
declare readonly _serviceBrand: undefined;
89
private readonly _providers = new Map<string, ISessionsProvider>();
90
register(p: { id: string }): void {
91
this._providers.set(p.id, p as unknown as ISessionsProvider);
92
}
93
override getProvider<T extends ISessionsProvider>(id: string): T | undefined {
94
return this._providers.get(id) as T | undefined;
95
}
96
override getProviders(): ISessionsProvider[] {
97
return [...this._providers.values()];
98
}
99
}
100
101
suite('agentHostSkillButtons - IsAgentHostSession context key', () => {
102
103
const store = ensureNoDisposablesAreLeakedInTestSuite();
104
105
function setup() {
106
const contextKeyService = store.add(new MockContextKeyService());
107
const sessions = new FakeSessionsManagementService();
108
const providers = new FakeSessionsProvidersService();
109
110
const instantiationService = store.add(new TestInstantiationService());
111
instantiationService.stub(IContextKeyService, contextKeyService);
112
instantiationService.stub(ISessionsManagementService, sessions);
113
instantiationService.stub(ISessionsProvidersService, providers);
114
115
store.add(instantiationService.createInstance(IsAgentHostSessionContextContribution));
116
117
return { contextKeyService, sessions, providers };
118
}
119
120
test('is false when no active session', () => {
121
const { contextKeyService } = setup();
122
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);
123
});
124
125
test('is true when active session comes from an agent-host provider', () => {
126
const { contextKeyService, sessions, providers } = setup();
127
providers.register(new FakeAgentHostProvider('local-agent-host'));
128
sessions.setActive(makeActiveSession('local-agent-host'));
129
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), true);
130
});
131
132
test('is false when active session comes from a non agent-host provider', () => {
133
const { contextKeyService, sessions, providers } = setup();
134
providers.register(new FakeNonAgentHostProvider('copilot-cloud-agent'));
135
sessions.setActive(makeActiveSession('copilot-cloud-agent'));
136
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);
137
});
138
139
test('is false when active session references an unknown provider', () => {
140
const { contextKeyService, sessions } = setup();
141
sessions.setActive(makeActiveSession('no-such-provider'));
142
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);
143
});
144
145
test('updates reactively when active session changes', () => {
146
const { contextKeyService, sessions, providers } = setup();
147
providers.register(new FakeAgentHostProvider('local-agent-host'));
148
providers.register(new FakeNonAgentHostProvider('copilot-cloud-agent'));
149
150
sessions.setActive(makeActiveSession('local-agent-host'));
151
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), true);
152
153
sessions.setActive(makeActiveSession('copilot-cloud-agent'));
154
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);
155
156
sessions.setActive(undefined);
157
assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);
158
});
159
});
160
161
suite('agentHostSkillButtons - menu registration', () => {
162
163
ensureNoDisposablesAreLeakedInTestSuite();
164
165
function skillButtonItems() {
166
const all = MenuRegistry.getMenuItems(MenuId.ChatEditingSessionApplySubmenu);
167
const menuItems: { command: { id: string }; when?: ContextKeyExpression }[] = [];
168
for (const item of all) {
169
if (!isIMenuItem(item)) {
170
continue;
171
}
172
if (isAgentHostSkillButtonId(item.command.id)) {
173
menuItems.push(item);
174
}
175
}
176
return menuItems;
177
}
178
179
test('registers four skill button menu items on the apply submenu', () => {
180
const ids = skillButtonItems().map(item => item.command.id).sort();
181
assert.deepStrictEqual(ids, [
182
'workbench.action.agentSessions.runSkill.createDraftPR',
183
'workbench.action.agentSessions.runSkill.createPR',
184
'workbench.action.agentSessions.runSkill.merge',
185
'workbench.action.agentSessions.runSkill.updatePR',
186
]);
187
});
188
189
test('every skill button `when` clause includes sessions.isAgentHostSession and isSessionsWindow', () => {
190
for (const item of skillButtonItems()) {
191
const whenStr = item.when?.serialize() ?? '';
192
assert.ok(
193
whenStr.includes(IsAgentHostSession.key),
194
`expected ${item.command.id} to gate on ${IsAgentHostSession.key}, got: ${whenStr}`,
195
);
196
assert.ok(
197
whenStr.includes('isSessionsWindow'),
198
`expected ${item.command.id} to gate on isSessionsWindow, got: ${whenStr}`,
199
);
200
}
201
});
202
203
test('exported updatePR id matches the registered command', () => {
204
assert.ok(isAgentHostSkillButtonId(AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID));
205
assert.ok(CommandsRegistry.getCommand(AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID),
206
`expected command ${AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID} to be registered`);
207
});
208
209
test('the apply submenu is contributed to the changes toolbar in the navigation group', () => {
210
const toolbarItems = MenuRegistry.getMenuItems(MenuId.ChatEditingSessionChangesToolbar);
211
const submenuEntry = toolbarItems.find(item => isISubmenuItem(item) && item.submenu === MenuId.ChatEditingSessionApplySubmenu);
212
assert.ok(submenuEntry, 'expected ChatEditingSessionApplySubmenu to be registered on ChatEditingSessionChangesToolbar');
213
assert.strictEqual((submenuEntry as { group?: string }).group, 'navigation');
214
});
215
216
test('isAgentHostSkillButtonId only matches our prefix', () => {
217
assert.strictEqual(isAgentHostSkillButtonId('workbench.action.agentSessions.runSkill.merge'), true);
218
assert.strictEqual(isAgentHostSkillButtonId('github.copilot.sessions.commit'), false);
219
assert.strictEqual(isAgentHostSkillButtonId(''), false);
220
});
221
});
222
223