Path: blob/main/extensions/copilot/src/lib/vscode-node/test/getInlineCompletions.spec.ts
13399 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*--------------------------------------------------------------------------------------------*/45// Load env6import * as dotenv from 'dotenv';7dotenv.config({ path: '../.env' });89import { readFile } from 'fs/promises';10import { join } from 'path';11import { assert, describe, expect, it } from 'vitest';12import type { AuthenticationGetSessionOptions, AuthenticationSession, ChatRequest, LanguageModelChat } from 'vscode';13import { ResultType } from '../../../extension/completions-core/vscode-node/lib/src/ghostText/resultType';14import { createTextDocument } from '../../../extension/completions-core/vscode-node/lib/src/test/textDocument';15import { TextDocumentIdentifier } from '../../../extension/completions-core/vscode-node/lib/src/textDocument';16import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '../../../extension/completions-core/vscode-node/lib/src/textDocumentManager';17import { CopilotToken, createTestExtendedTokenInfo } from '../../../platform/authentication/common/copilotToken';18import { ChatEndpointFamily, EmbeddingsEndpointFamily } from '../../../platform/endpoint/common/endpointProvider';19import { MutableObservableWorkspace } from '../../../platform/inlineEdits/common/observableWorkspace';20import { FetchOptions, IAbortController, IHeaders, PaginationOptions, Response } from '../../../platform/networking/common/fetcherService';21import { IChatEndpoint, IEmbeddingsEndpoint, IFetcher } from '../../../platform/networking/common/networking';22import { Emitter, Event } from '../../../util/vs/base/common/event';23import { Disposable } from '../../../util/vs/base/common/lifecycle';24import { URI } from '../../../util/vs/base/common/uri';25import { createInlineCompletionsProvider, IActionItem, IAuthenticationService, ICompletionsStatusChangedEvent, ICompletionsTextDocumentManager, IEndpointProvider, ILogTarget, ITelemetrySender, LogLevel } from '../../node/chatLibMain';2627class TestFetcher implements IFetcher {28private _fetched = new Map<string, number>();2930constructor(private readonly responses: Record<string, string>) { }3132getUserAgentLibrary(): string {33return 'TestFetcher'; // matches the naming convention inside of completions34}3536async fetch(url: string, options: FetchOptions): Promise<Response> {37const uri = URI.parse(url);38this._markFetched(uri.path);39const responseText = this.responses[uri.path];4041const headers = new class implements IHeaders {42get(name: string): string | null {43return null;44}45*[Symbol.iterator](): Iterator<[string, string]> {46// Empty headers for test47}48};4950const found = typeof responseText === 'string';51const text = responseText || '';52return Response.fromText(53found ? 200 : 404,54found ? 'OK' : 'Not Found',55headers,56text,57'node-http'58);59}6061private _markFetched(urlPath: string): void {62const count = this.fetchCount(urlPath);63this._fetched.set(urlPath, count + 1);64}6566fetchCount(urlPath: string): number {67return this._fetched.get(urlPath) || 0;68}6970fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {71throw new Error('Method not implemented.');72}7374async disconnectAll(): Promise<unknown> {75return Promise.resolve();76}7778makeAbortController(): IAbortController {79return new AbortController();80}8182isAbortError(e: any): boolean {83return e && e.name === 'AbortError';84}8586isInternetDisconnectedError(e: any): boolean {87return false;88}8990isFetcherError(e: any): boolean {91return false;92}9394isNetworkProcessCrashedError(e: any): boolean {95return false;96}9798getUserMessageForFetcherError(err: any): string {99return `Test fetcher error: ${err.message}`;100}101}102103function createTestCopilotToken(): CopilotToken {104return new CopilotToken(createTestExtendedTokenInfo({105token: `test token ${Math.ceil(Math.random() * 100)}`,106}));107}108109class TestAuthService extends Disposable implements IAuthenticationService {110readonly _serviceBrand: undefined;111readonly isMinimalMode = true;112readonly anyGitHubSession = undefined;113readonly permissiveGitHubSession = undefined;114readonly copilotToken = createTestCopilotToken();115speculativeDecodingEndpointToken: string | undefined;116117private readonly _onDidAuthenticationChange = this._register(new Emitter<void>());118readonly onDidAuthenticationChange: Event<void> = this._onDidAuthenticationChange.event;119120private readonly _onDidAccessTokenChange = this._register(new Emitter<void>());121readonly onDidAccessTokenChange = this._onDidAccessTokenChange.event;122123private readonly _onDidAdoAuthenticationChange = this._register(new Emitter<void>());124readonly onDidAdoAuthenticationChange = this._onDidAdoAuthenticationChange.event;125126async getGitHubSession(kind: 'permissive' | 'any', options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession> {127throw new Error('Method not implemented.');128}129130async getCopilotToken(force?: boolean): Promise<CopilotToken> {131return this.copilotToken;132}133134resetCopilotToken(httpError?: number): void { }135136async getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise<string | undefined> {137return undefined;138}139}140141class TestTelemetrySender implements ITelemetrySender {142events: { eventName: string; properties?: Record<string, string | undefined>; measurements?: Record<string, number | undefined> }[] = [];143sendTelemetryEvent(eventName: string, properties?: Record<string, string | undefined>, measurements?: Record<string, number | undefined>): void {144this.events.push({ eventName, properties, measurements });145}146}147148class TestEndpointProvider implements IEndpointProvider {149readonly _serviceBrand: undefined;150readonly onDidModelsRefresh = Event.None;151152async getAllCompletionModels(forceRefresh?: boolean) {153return [];154}155156async getAllChatEndpoints() {157return [];158}159160async getChatEndpoint(requestOrFamily: LanguageModelChat | ChatRequest | ChatEndpointFamily): Promise<IChatEndpoint> {161throw new Error('Method not implemented.');162}163164async getEmbeddingsEndpoint(family?: EmbeddingsEndpointFamily): Promise<IEmbeddingsEndpoint> {165throw new Error('Method not implemented.');166}167}168169class TestDocumentManager extends Disposable implements ICompletionsTextDocumentManager {170private readonly _onDidChangeTextDocument = this._register(new Emitter<TextDocumentChangeEvent>());171readonly onDidChangeTextDocument = this._onDidChangeTextDocument.event;172173private readonly _onDidOpenTextDocument = this._register(new Emitter<TextDocumentOpenEvent>());174readonly onDidOpenTextDocument = this._onDidOpenTextDocument.event;175176private readonly _onDidCloseTextDocument = this._register(new Emitter<TextDocumentCloseEvent>());177readonly onDidCloseTextDocument = this._onDidCloseTextDocument.event;178179private readonly _onDidFocusTextDocument = this._register(new Emitter<TextDocumentFocusedEvent>());180readonly onDidFocusTextDocument = this._onDidFocusTextDocument.event;181182private readonly _onDidChangeWorkspaceFolders = this._register(new Emitter<WorkspaceFoldersChangeEvent>());183readonly onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event;184185getTextDocumentsUnsafe() {186return [];187}188189findNotebook(doc: TextDocumentIdentifier) {190return undefined;191}192193getWorkspaceFolders() {194return [];195}196}197198class NullLogTarget implements ILogTarget {199logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void { }200}201202describe('getInlineCompletions', () => {203const completionsPath = '/v1/engines/gpt-41-copilot/completions';204let fetcher: TestFetcher;205206async function getCompletionsProvider() {207fetcher = new TestFetcher({ [completionsPath]: await readFile(join(__dirname, 'getInlineCompletions.reply.txt'), 'utf8') });208209return createInlineCompletionsProvider({210fetcher,211authService: new TestAuthService(),212telemetrySender: new TestTelemetrySender(),213logTarget: new NullLogTarget(),214isRunningInTest: true,215contextProviderMatch: async () => 0,216statusHandler: new class { didChange(_: ICompletionsStatusChangedEvent) { } },217documentManager: new TestDocumentManager(),218workspace: new MutableObservableWorkspace(),219urlOpener: new class {220async open(_url: string) { }221},222editorInfo: { name: 'test-editor', version: '1.0.0' },223editorPluginInfo: { name: 'test-plugin', version: '1.0.0' },224relatedPluginInfo: [],225editorSession: {226sessionId: 'test-session-id',227machineId: 'test-machine-id',228},229notificationSender: new class {230async showWarningMessage(_message: string, ..._items: IActionItem[]) { return undefined; }231},232endpointProvider: new TestEndpointProvider(),233});234}235236it('should return completions for a document and position', async () => {237const provider = await getCompletionsProvider();238const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n}\n');239240const result = await provider.getInlineCompletions(doc, { line: 1, character: 0 });241242assert(result);243expect(result.length).toBe(1);244expect(result[0].resultType).toBe(ResultType.Async);245expect(result[0].displayText).toBe(' console.log("Hello, World!");');246});247248it('makes any pending speculative requests when a completion is shown', async () => {249const provider = await getCompletionsProvider();250const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n}\n');251252const result = await provider.getInlineCompletions(doc, { line: 1, character: 0 });253254assert(result);255expect(result.length).toBe(1);256257await provider.inlineCompletionShown(result[0].clientCompletionId);258259expect(fetcher.fetchCount(completionsPath)).toBe(2);260});261});262263264