Path: blob/main/src/vs/sessions/contrib/chat/test/browser/agentHost/agentHostPermissionPickerDelegate.test.ts
13406 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 { Emitter, Event } from '../../../../../../base/common/event.js';7import { DisposableStore } from '../../../../../../base/common/lifecycle.js';8import { observableValue } from '../../../../../../base/common/observable.js';9import { mock } from '../../../../../../base/test/common/mock.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';11import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';12import { ResolveSessionConfigResult, SessionConfigPropertySchema } from '../../../../../../platform/agentHost/common/state/protocol/commands.js';13import { ChatPermissionLevel } from '../../../../../../workbench/contrib/chat/common/constants.js';14import { AgentHostPermissionPickerDelegate, isWellKnownAutoApproveSchema, isWellKnownModeSchema } from '../../../browser/agentHost/agentHostPermissionPickerDelegate.js';15import { IAgentHostSessionsProvider } from '../../../../../common/agentHostSessionsProvider.js';16import { ISessionsProvidersChangeEvent, ISessionsProvidersService } from '../../../../../services/sessions/browser/sessionsProvidersService.js';17import { ISessionsProvider } from '../../../../../services/sessions/common/sessionsProvider.js';18import { IActiveSession, ISessionsManagementService } from '../../../../../services/sessions/common/sessionsManagement.js';1920const PROVIDER_ID = 'local-agent-host';21const SESSION_ID = 'local-agent-host:s1';2223function makeWellKnownConfig(value: string | undefined): ResolveSessionConfigResult {24return {25schema: {26type: 'object',27properties: {28autoApprove: {29title: 'Auto Approve',30description: '',31type: 'string',32enum: ['default', 'autoApprove', 'autopilot'],33sessionMutable: true,34},35},36},37values: value === undefined ? {} : { autoApprove: value },38} as ResolveSessionConfigResult;39}4041class FakeProvider implements Pick<IAgentHostSessionsProvider, 'id' | 'onDidChangeSessionConfig' | 'getSessionConfig' | 'setSessionConfigValue'> {42readonly id: string = PROVIDER_ID;43private readonly _onDidChange = new Emitter<string>();44readonly onDidChangeSessionConfig: Event<string> = this._onDidChange.event;4546config: ResolveSessionConfigResult | undefined;47readonly setCalls: Array<[string, string, string]> = [];4849getSessionConfig(_sessionId: string): ResolveSessionConfigResult | undefined {50return this.config;51}52async setSessionConfigValue(sessionId: string, property: string, value: string): Promise<void> {53this.setCalls.push([sessionId, property, value]);54}55fireChange(sessionId: string = SESSION_ID): void {56this._onDidChange.fire(sessionId);57}58dispose(): void {59this._onDidChange.dispose();60}61}6263interface ITestRig {64readonly delegate: AgentHostPermissionPickerDelegate;65readonly provider: FakeProvider;66readonly activeSessionObs: ReturnType<typeof observableValue<IActiveSession | undefined>>;67}6869function setup(store: Pick<DisposableStore, 'add'>, activeSession: IActiveSession | undefined, configValue?: string): ITestRig {70const provider = new FakeProvider();71store.add({ dispose: () => provider.dispose() });72if (configValue !== undefined) {73provider.config = makeWellKnownConfig(configValue);74}75const onDidChangeProviders = store.add(new Emitter<ISessionsProvidersChangeEvent>());76const sessionsProvidersService = new (class extends mock<ISessionsProvidersService>() {77override readonly onDidChangeProviders = onDidChangeProviders.event;78override getProviders(): ISessionsProvider[] { return [provider as unknown as ISessionsProvider]; }79override getProvider<T extends ISessionsProvider>(id: string): T | undefined {80return id === provider.id ? (provider as unknown as T) : undefined;81}82})();83const activeSessionObs = observableValue<IActiveSession | undefined>('activeSession', activeSession);84const sessionsManagementService = new (class extends mock<ISessionsManagementService>() {85override readonly activeSession = activeSessionObs;86})();8788const insta = store.add(new TestInstantiationService());89insta.set(ISessionsManagementService, sessionsManagementService);90insta.set(ISessionsProvidersService, sessionsProvidersService);9192const delegate = store.add(insta.createInstance(AgentHostPermissionPickerDelegate));93return { delegate, provider, activeSessionObs };94}9596function makeActiveSession(): IActiveSession {97return { providerId: PROVIDER_ID, sessionId: SESSION_ID } as IActiveSession;98}99100suite('AgentHostPermissionPickerDelegate', () => {101const store = ensureNoDisposablesAreLeakedInTestSuite();102103test('returns Default when there is no active session', () => {104const { delegate } = setup(store, undefined);105106assert.strictEqual(delegate.currentPermissionLevel.get(), ChatPermissionLevel.Default);107});108109test('returns Default when the active session has no config seeded yet', () => {110const { delegate } = setup(store, makeActiveSession());111112assert.strictEqual(delegate.currentPermissionLevel.get(), ChatPermissionLevel.Default);113});114115test('reflects the active session\'s autoApprove value and updates on provider change', () => {116const { delegate, provider } = setup(store, makeActiveSession(), 'autoApprove');117118assert.strictEqual(delegate.currentPermissionLevel.get(), ChatPermissionLevel.AutoApprove);119120provider.config = makeWellKnownConfig('autopilot');121provider.fireChange();122assert.strictEqual(delegate.currentPermissionLevel.get(), ChatPermissionLevel.Autopilot);123124provider.config = makeWellKnownConfig('default');125provider.fireChange();126assert.strictEqual(delegate.currentPermissionLevel.get(), ChatPermissionLevel.Default);127});128129test('falls back to Default when the stored value is unrecognized', () => {130const { delegate } = setup(store, makeActiveSession(), 'something-else');131132assert.strictEqual(delegate.currentPermissionLevel.get(), ChatPermissionLevel.Default);133});134135test('setPermissionLevel writes through to the active session\'s provider', () => {136const { delegate, provider } = setup(store, makeActiveSession(), 'default');137138delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove);139delegate.setPermissionLevel(ChatPermissionLevel.Autopilot);140141assert.deepStrictEqual(provider.setCalls, [142[SESSION_ID, 'autoApprove', 'autoApprove'],143[SESSION_ID, 'autoApprove', 'autopilot'],144]);145});146147test('setPermissionLevel is a no-op when there is no active session', () => {148const { delegate, provider } = setup(store, undefined);149150delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove);151152assert.deepStrictEqual(provider.setCalls, []);153});154155test('isApplicable reacts to active session and config changes', () => {156const { delegate, provider, activeSessionObs } = setup(store, undefined);157158// No active session → false159assert.strictEqual(delegate.isApplicable.get(), false);160161// Active session, no config seeded → false162activeSessionObs.set(makeActiveSession(), undefined);163assert.strictEqual(delegate.isApplicable.get(), false);164165// Active session with well-known schema → true166provider.config = makeWellKnownConfig('default');167provider.fireChange();168assert.strictEqual(delegate.isApplicable.get(), true);169170// Active session cleared → false (covers the 'back to new chat view' regression)171activeSessionObs.set(undefined, undefined);172assert.strictEqual(delegate.isApplicable.get(), false);173});174});175176suite('isWellKnownAutoApproveSchema', () => {177ensureNoDisposablesAreLeakedInTestSuite();178179function schema(overrides: Partial<SessionConfigPropertySchema> = {}): SessionConfigPropertySchema {180return {181title: 'Auto Approve',182description: 'desc',183type: 'string',184enum: ['default', 'autoApprove', 'autopilot'],185...overrides,186} as SessionConfigPropertySchema;187}188189test('matches the canonical three-value enum', () => {190assert.strictEqual(isWellKnownAutoApproveSchema(schema()), true);191});192193test('matches a subset that still contains "default"', () => {194assert.strictEqual(isWellKnownAutoApproveSchema(schema({ enum: ['default', 'autoApprove'] })), true);195assert.strictEqual(isWellKnownAutoApproveSchema(schema({ enum: ['default'] })), true);196});197198test('rejects schemas missing the required "default" value', () => {199assert.strictEqual(isWellKnownAutoApproveSchema(schema({ enum: ['autoApprove', 'autopilot'] })), false);200});201202test('rejects schemas with unknown enum values', () => {203assert.strictEqual(isWellKnownAutoApproveSchema(schema({ enum: ['default', 'custom'] })), false);204});205206test('rejects non-string types and missing/empty enums', () => {207assert.strictEqual(isWellKnownAutoApproveSchema(schema({ type: 'number' as 'string' })), false);208assert.strictEqual(isWellKnownAutoApproveSchema(schema({ enum: undefined })), false);209assert.strictEqual(isWellKnownAutoApproveSchema(schema({ enum: [] })), false);210});211});212213suite('isWellKnownModeSchema', () => {214ensureNoDisposablesAreLeakedInTestSuite();215216function schema(overrides: Partial<SessionConfigPropertySchema> = {}): SessionConfigPropertySchema {217return {218title: 'Agent Mode',219description: 'desc',220type: 'string',221enum: ['interactive', 'plan'],222...overrides,223} as SessionConfigPropertySchema;224}225226test('matches the canonical two-value enum', () => {227assert.strictEqual(isWellKnownModeSchema(schema()), true);228});229230test('matches a subset that still contains "interactive"', () => {231assert.strictEqual(isWellKnownModeSchema(schema({ enum: ['interactive'] })), true);232});233234test('rejects schemas missing the required "interactive" value', () => {235assert.strictEqual(isWellKnownModeSchema(schema({ enum: ['plan'] })), false);236});237238test('rejects non-string types and missing/empty enums', () => {239assert.strictEqual(isWellKnownModeSchema(schema({ type: 'number' as 'string' })), false);240assert.strictEqual(isWellKnownModeSchema(schema({ enum: undefined })), false);241assert.strictEqual(isWellKnownModeSchema(schema({ enum: [] })), false);242});243});244245246