Path: blob/main/src/vs/workbench/contrib/editTelemetry/test/browser/aiContributionFeature.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 { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { URI } from '../../../../../base/common/uri.js';8import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';9import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';10import { AnnotatedDocuments, UriVisibilityProvider } from '../../browser/helpers/annotatedDocuments.js';11import { StringEditWithReason } from '../../browser/helpers/observableWorkspace.js';12import { AiContributionFeature } from '../../browser/aiContributionFeature.js';13import { EditSources } from '../../../../../editor/common/textModelEditSource.js';14import { DiffService } from '../../browser/helpers/documentWithAnnotatedEdits.js';15import { computeStringDiff } from '../../../../../editor/common/services/editorWebWorker.js';16import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';17import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';18import { MutableObservableWorkspace } from './editTelemetry.test.js';19import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';20import { timeout } from '../../../../../base/common/async.js';21import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';2223suite('AiContributionFeature', () => {24ensureNoDisposablesAreLeakedInTestSuite();2526let disposables: DisposableStore;27let workspace: MutableObservableWorkspace;2829const fileA = URI.parse('file:///a.ts');30const fileB = URI.parse('file:///b.ts');3132const chatEdit = EditSources.chatApplyEdits({33languageId: 'plaintext',34modelId: undefined,35codeBlockSuggestionId: undefined,36extensionId: undefined,37mode: undefined,38requestId: undefined,39sessionId: undefined,40});4142const userEdit = EditSources.cursor({ kind: 'type' });4344const inlineCompletionEdit = EditSources.inlineCompletionAccept({45nes: false,46requestUuid: 'test-uuid',47languageId: 'plaintext',48correlationId: undefined,49});5051function setup(): void {52disposables = new DisposableStore();53const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection(), false, undefined, true));54instantiationService.stubInstance(DiffService, { computeDiff: async (original, modified) => computeStringDiff(original, modified, { maxComputationTimeMs: 500 }, 'advanced') });55instantiationService.stubInstance(UriVisibilityProvider, { isVisible: () => true });56instantiationService.stub(ILogService, new NullLogService());5758workspace = new MutableObservableWorkspace();59const annotatedDocuments = disposables.add(new AnnotatedDocuments(workspace, instantiationService));60disposables.add(instantiationService.createInstance(AiContributionFeature, annotatedDocuments));61}6263function hasAiContributions(uris: URI[], level: 'chatAndAgent' | 'all'): boolean {64return CommandsRegistry.getCommand('_aiEdits.hasAiContributions')!.handler(undefined!, uris, level) as unknown as boolean;65}6667function clearAiContributions(uris: URI[]): void {68CommandsRegistry.getCommand('_aiEdits.clearAiContributions')!.handler(undefined!, uris);69}7071function clearAllAiContributions(): void {72CommandsRegistry.getCommand('_aiEdits.clearAllAiContributions')!.handler(undefined!);73}7475test('no contributions initially', () => runWithFakedTimers({}, async () => {76setup();77const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));78await timeout(1500);79assert.strictEqual(hasAiContributions([d.uri], 'all'), false);80assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), false);81disposables.dispose();82}));8384test('detects chat AI edits', () => runWithFakedTimers({}, async () => {85setup();86const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));87await timeout(1500);8889d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', chatEdit));90await timeout(1500);9192assert.strictEqual(hasAiContributions([d.uri], 'all'), true);93assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), true);94disposables.dispose();95}));9697test('detects inline completion AI edits at all level only', () => runWithFakedTimers({}, async () => {98setup();99const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));100await timeout(1500);101102d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', inlineCompletionEdit));103await timeout(1500);104105assert.strictEqual(hasAiContributions([d.uri], 'all'), true);106assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), false);107disposables.dispose();108}));109110test('does not detect user edits as AI', () => runWithFakedTimers({}, async () => {111setup();112const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));113await timeout(1500);114115d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', userEdit));116await timeout(1500);117118assert.strictEqual(hasAiContributions([d.uri], 'all'), false);119assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), false);120disposables.dispose();121}));122123test('clear resets contributions for specific resources', () => runWithFakedTimers({}, async () => {124setup();125const dA = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));126const dB = disposables.add(workspace.createDocument({ uri: fileB, initialValue: 'world' }, undefined));127await timeout(1500);128129dA.applyEdit(StringEditWithReason.replace(dA.findRange('hello'), 'foo', chatEdit));130dB.applyEdit(StringEditWithReason.replace(dB.findRange('world'), 'bar', chatEdit));131await timeout(1500);132133assert.strictEqual(hasAiContributions([dA.uri], 'all'), true);134assert.strictEqual(hasAiContributions([dB.uri], 'all'), true);135136clearAiContributions([dA.uri]);137138assert.strictEqual(hasAiContributions([dA.uri], 'all'), false);139assert.strictEqual(hasAiContributions([dB.uri], 'all'), true);140disposables.dispose();141}));142143test('clearAll resets all contributions', () => runWithFakedTimers({}, async () => {144setup();145const dA = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));146const dB = disposables.add(workspace.createDocument({ uri: fileB, initialValue: 'world' }, undefined));147await timeout(1500);148149dA.applyEdit(StringEditWithReason.replace(dA.findRange('hello'), 'foo', chatEdit));150dB.applyEdit(StringEditWithReason.replace(dB.findRange('world'), 'bar', chatEdit));151await timeout(1500);152153clearAllAiContributions();154155assert.strictEqual(hasAiContributions([dA.uri], 'all'), false);156assert.strictEqual(hasAiContributions([dB.uri], 'all'), false);157disposables.dispose();158}));159160test('tracks new edits after clear', () => runWithFakedTimers({}, async () => {161setup();162const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));163await timeout(1500);164165d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', chatEdit));166await timeout(1500);167168clearAiContributions([d.uri]);169assert.strictEqual(hasAiContributions([d.uri], 'all'), false);170171d.applyEdit(StringEditWithReason.replace(d.findRange('world'), 'again', chatEdit));172await timeout(1500);173174assert.strictEqual(hasAiContributions([d.uri], 'all'), true);175disposables.dispose();176}));177178test('cleans up tracker when document is closed', () => runWithFakedTimers({}, async () => {179setup();180const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));181await timeout(1500);182183d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', chatEdit));184await timeout(1500);185186assert.strictEqual(hasAiContributions([d.uri], 'all'), true);187188d.dispose();189await timeout(1500);190191assert.strictEqual(hasAiContributions([fileA], 'all'), false);192disposables.dispose();193}));194195test('returns false for unknown URIs', () => runWithFakedTimers({}, async () => {196setup();197assert.strictEqual(hasAiContributions([URI.parse('file:///unknown.ts')], 'all'), false);198disposables.dispose();199}));200201test('checks multiple resources', () => runWithFakedTimers({}, async () => {202setup();203const dA = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));204disposables.add(workspace.createDocument({ uri: fileB, initialValue: 'world' }, undefined));205await timeout(1500);206207dA.applyEdit(StringEditWithReason.replace(dA.findRange('hello'), 'foo', chatEdit));208await timeout(1500);209210// Returns true if any of the resources has AI contributions211assert.strictEqual(hasAiContributions([fileA, fileB], 'all'), true);212assert.strictEqual(hasAiContributions([fileB, fileA], 'all'), true);213assert.strictEqual(hasAiContributions([fileB], 'all'), false);214disposables.dispose();215}));216});217218219