Path: blob/main/src/vs/sessions/contrib/agentHost/test/browser/agentHostSkillButtons.test.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { Codicon } from '../../../../../base/common/codicons.js';7import { observableValue } from '../../../../../base/common/observable.js';8import { URI } from '../../../../../base/common/uri.js';9import { mock } from '../../../../../base/test/common/mock.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { isIMenuItem, isISubmenuItem, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js';12import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';13import { ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';14import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';15import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';16import { IChat } from '../../../../services/sessions/common/session.js';17import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';18import { ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js';19import { IActiveSession, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';20import { AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID, IsAgentHostSession, IsAgentHostSessionContextContribution, isAgentHostSkillButtonId } from '../../browser/agentHostSkillButtons.js';21import { BaseAgentHostSessionsProvider } from '../../browser/baseAgentHostSessionsProvider.js';22// Importing this contribution registers the apply submenu on the changes toolbar,23// which is the slot that hosts our skill buttons as a dropdown.24import '../../../applyCommitsToParentRepo/browser/applyChangesToParentRepo.js';2526function makeActiveSession(providerId: string): IActiveSession {27const chat: IChat = {28resource: URI.parse('file:///session'),29createdAt: new Date(),30title: observableValue('t', 'Test'),31updatedAt: observableValue('u', new Date()),32status: observableValue('s', 0),33changes: observableValue('c', []),34modelId: observableValue('m', undefined),35mode: observableValue('mo', undefined),36isArchived: observableValue('ia', false),37isRead: observableValue('ir', true),38lastTurnEnd: observableValue('lte', undefined),39description: observableValue('d', undefined),40};41return {42sessionId: `${providerId}:x`,43resource: chat.resource,44providerId,45sessionType: 'copilotcli',46icon: Codicon.copilot,47createdAt: chat.createdAt,48workspace: observableValue('w', undefined),49title: chat.title,50updatedAt: chat.updatedAt,51status: chat.status,52changes: chat.changes,53modelId: chat.modelId,54mode: chat.mode,55loading: observableValue('l', false),56isArchived: chat.isArchived,57isRead: chat.isRead,58lastTurnEnd: chat.lastTurnEnd,59description: chat.description,60gitHubInfo: observableValue('gh', undefined),61chats: observableValue('chats', [chat]),62activeChat: observableValue('ac', chat),63mainChat: chat,64capabilities: { supportsMultipleChats: false },65} as IActiveSession;66}6768class FakeAgentHostProvider {69constructor(public readonly id: string) { }70}71// Make `instanceof BaseAgentHostSessionsProvider` return true without actually constructing one.72Object.setPrototypeOf(FakeAgentHostProvider.prototype, BaseAgentHostSessionsProvider.prototype);7374class FakeNonAgentHostProvider {75constructor(public readonly id: string) { }76}7778class FakeSessionsManagementService extends mock<ISessionsManagementService>() {79declare readonly _serviceBrand: undefined;80override readonly activeSession = observableValue<IActiveSession | undefined>('activeSession', undefined);81setActive(s: IActiveSession | undefined): void {82this.activeSession.set(s, undefined);83}84}8586class FakeSessionsProvidersService extends mock<ISessionsProvidersService>() {87declare readonly _serviceBrand: undefined;88private readonly _providers = new Map<string, ISessionsProvider>();89register(p: { id: string }): void {90this._providers.set(p.id, p as unknown as ISessionsProvider);91}92override getProvider<T extends ISessionsProvider>(id: string): T | undefined {93return this._providers.get(id) as T | undefined;94}95override getProviders(): ISessionsProvider[] {96return [...this._providers.values()];97}98}99100suite('agentHostSkillButtons - IsAgentHostSession context key', () => {101102const store = ensureNoDisposablesAreLeakedInTestSuite();103104function setup() {105const contextKeyService = store.add(new MockContextKeyService());106const sessions = new FakeSessionsManagementService();107const providers = new FakeSessionsProvidersService();108109const instantiationService = store.add(new TestInstantiationService());110instantiationService.stub(IContextKeyService, contextKeyService);111instantiationService.stub(ISessionsManagementService, sessions);112instantiationService.stub(ISessionsProvidersService, providers);113114store.add(instantiationService.createInstance(IsAgentHostSessionContextContribution));115116return { contextKeyService, sessions, providers };117}118119test('is false when no active session', () => {120const { contextKeyService } = setup();121assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);122});123124test('is true when active session comes from an agent-host provider', () => {125const { contextKeyService, sessions, providers } = setup();126providers.register(new FakeAgentHostProvider('local-agent-host'));127sessions.setActive(makeActiveSession('local-agent-host'));128assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), true);129});130131test('is false when active session comes from a non agent-host provider', () => {132const { contextKeyService, sessions, providers } = setup();133providers.register(new FakeNonAgentHostProvider('copilot-cloud-agent'));134sessions.setActive(makeActiveSession('copilot-cloud-agent'));135assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);136});137138test('is false when active session references an unknown provider', () => {139const { contextKeyService, sessions } = setup();140sessions.setActive(makeActiveSession('no-such-provider'));141assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);142});143144test('updates reactively when active session changes', () => {145const { contextKeyService, sessions, providers } = setup();146providers.register(new FakeAgentHostProvider('local-agent-host'));147providers.register(new FakeNonAgentHostProvider('copilot-cloud-agent'));148149sessions.setActive(makeActiveSession('local-agent-host'));150assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), true);151152sessions.setActive(makeActiveSession('copilot-cloud-agent'));153assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);154155sessions.setActive(undefined);156assert.strictEqual(contextKeyService.getContextKeyValue(IsAgentHostSession.key), false);157});158});159160suite('agentHostSkillButtons - menu registration', () => {161162ensureNoDisposablesAreLeakedInTestSuite();163164function skillButtonItems() {165const all = MenuRegistry.getMenuItems(MenuId.ChatEditingSessionApplySubmenu);166const menuItems: { command: { id: string }; when?: ContextKeyExpression }[] = [];167for (const item of all) {168if (!isIMenuItem(item)) {169continue;170}171if (isAgentHostSkillButtonId(item.command.id)) {172menuItems.push(item);173}174}175return menuItems;176}177178test('registers four skill button menu items on the apply submenu', () => {179const ids = skillButtonItems().map(item => item.command.id).sort();180assert.deepStrictEqual(ids, [181'workbench.action.agentSessions.runSkill.createDraftPR',182'workbench.action.agentSessions.runSkill.createPR',183'workbench.action.agentSessions.runSkill.merge',184'workbench.action.agentSessions.runSkill.updatePR',185]);186});187188test('every skill button `when` clause includes sessions.isAgentHostSession and isSessionsWindow', () => {189for (const item of skillButtonItems()) {190const whenStr = item.when?.serialize() ?? '';191assert.ok(192whenStr.includes(IsAgentHostSession.key),193`expected ${item.command.id} to gate on ${IsAgentHostSession.key}, got: ${whenStr}`,194);195assert.ok(196whenStr.includes('isSessionsWindow'),197`expected ${item.command.id} to gate on isSessionsWindow, got: ${whenStr}`,198);199}200});201202test('exported updatePR id matches the registered command', () => {203assert.ok(isAgentHostSkillButtonId(AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID));204assert.ok(CommandsRegistry.getCommand(AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID),205`expected command ${AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID} to be registered`);206});207208test('the apply submenu is contributed to the changes toolbar in the navigation group', () => {209const toolbarItems = MenuRegistry.getMenuItems(MenuId.ChatEditingSessionChangesToolbar);210const submenuEntry = toolbarItems.find(item => isISubmenuItem(item) && item.submenu === MenuId.ChatEditingSessionApplySubmenu);211assert.ok(submenuEntry, 'expected ChatEditingSessionApplySubmenu to be registered on ChatEditingSessionChangesToolbar');212assert.strictEqual((submenuEntry as { group?: string }).group, 'navigation');213});214215test('isAgentHostSkillButtonId only matches our prefix', () => {216assert.strictEqual(isAgentHostSkillButtonId('workbench.action.agentSessions.runSkill.merge'), true);217assert.strictEqual(isAgentHostSkillButtonId('github.copilot.sessions.commit'), false);218assert.strictEqual(isAgentHostSkillButtonId(''), false);219});220});221222223