Path: blob/main/extensions/copilot/src/lib/vscode-node/test/nesProvider.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 { promises as fs } from 'fs';10import { outdent } from 'outdent';11import * as path from 'path';12import { assert, describe, expect, it } from 'vitest';13import { CopilotToken, createTestExtendedTokenInfo } from '../../../platform/authentication/common/copilotToken';14import { ICopilotTokenManager } from '../../../platform/authentication/common/copilotTokenManager';15import { DocumentId } from '../../../platform/inlineEdits/common/dataTypes/documentId';16import { MutableObservableWorkspace } from '../../../platform/inlineEdits/common/observableWorkspace';17import { FetchOptions, IAbortController, IHeaders, PaginationOptions, Response } from '../../../platform/networking/common/fetcherService';18import { IFetcher } from '../../../platform/networking/common/networking';19import { NullTerminalService } from '../../../platform/terminal/common/terminalService';20import { CancellationToken } from '../../../util/vs/base/common/cancellation';21import { Emitter } from '../../../util/vs/base/common/event';22import { URI } from '../../../util/vs/base/common/uri';23import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';24import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';25import { createNESProvider, ILogTarget, ITelemetrySender, LogLevel } from '../../node/chatLibMain';262728class TestFetcher implements IFetcher {2930requests: { url: string; options: FetchOptions }[] = [];3132constructor(private readonly responses: Record<string, string>) { }3334getUserAgentLibrary(): string {35return 'test-fetcher';36}3738async fetch(url: string, options: FetchOptions): Promise<Response> {39this.requests.push({ url, options });40const uri = URI.parse(url);41const responseText = this.responses[uri.path];4243const headers = new class implements IHeaders {44get(name: string): string | null {45return null;46}47*[Symbol.iterator](): Iterator<[string, string]> {48// Empty headers for test49}50};5152const found = typeof responseText === 'string';53const text = responseText || '';54return Response.fromText(55found ? 200 : 404,56found ? 'OK' : 'Not Found',57headers,58text,59'node-http'60);61}6263fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {64throw new Error('Method not implemented.');65}6667async disconnectAll(): Promise<unknown> {68return Promise.resolve();69}7071makeAbortController(): IAbortController {72return new AbortController();73}7475isAbortError(e: any): boolean {76return e && e.name === 'AbortError';77}7879isInternetDisconnectedError(e: any): boolean {80return false;81}8283isFetcherError(e: any): boolean {84return false;85}8687isNetworkProcessCrashedError(e: any): boolean {88return false;89}9091getUserMessageForFetcherError(err: any): string {92return `Test fetcher error: ${err.message}`;93}94}9596class TestCopilotTokenManager implements ICopilotTokenManager {97_serviceBrand: undefined;9899onDidCopilotTokenRefresh = new Emitter<void>().event;100101async getCopilotToken(force?: boolean): Promise<CopilotToken> {102return new CopilotToken(createTestExtendedTokenInfo({ token: 'fixedToken' }));103}104105resetCopilotToken(httpError?: number): void {106// nothing107}108}109110class TestTelemetrySender implements ITelemetrySender {111events: { eventName: string; properties?: Record<string, string | undefined>; measurements?: Record<string, number | undefined> }[] = [];112sendTelemetryEvent(eventName: string, properties?: Record<string, string | undefined>, measurements?: Record<string, number | undefined>): void {113this.events.push({ eventName, properties, measurements });114}115}116117class TestLogTarget implements ILogTarget {118logs: { level: LogLevel; message: string; metadata?: any }[] = [];119logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void {120this.logs.push({ level, message: metadataStr, metadata: extra });121console.log(`[${LogLevel[level]}]${metadataStr}`, ...extra);122}123}124125describe('NESProvider Facade', () => {126it('should handle getNextEdit call with a document URI', async () => {127const workspace = new MutableObservableWorkspace();128const doc = workspace.addDocument({129id: DocumentId.create(URI.file('/test/test.ts').toString()),130initialValue: outdent`131class Point {132constructor(133private readonly x: number,134private readonly y: number,135) { }136getDistance() {137return Math.sqrt(this.x ** 2 + this.y ** 2);138}139}140141const myPoint = new Point(0, 1);`.trimStart()142});143doc.setSelection([new OffsetRange(1, 1)], undefined);144const telemetrySender = new TestTelemetrySender();145const terminalService = new NullTerminalService();146const logTarget = new TestLogTarget();147const fetcher = new TestFetcher({148'/models': JSON.stringify({ models: [] }),149'/chat/completions': await fs.readFile(path.join(__dirname, 'nesProvider.reply.txt'), 'utf8'),150});151const nextEditProvider = createNESProvider({152workspace,153fetcher,154copilotTokenManager: new TestCopilotTokenManager(),155telemetrySender,156terminalService,157logTarget,158});159nextEditProvider.updateTreatmentVariables({160'config.github.copilot.chat.advanced.inlineEdits.xtabProvider.defaultModelConfigurationString': '{ "modelName": "xtab-test", "promptingStrategy": "copilotNesXtab", "includeTagsInCurrentFile": false }',161});162163doc.applyEdit(StringEdit.insert(11, '3D'));164165const result = await nextEditProvider.getNextEdit(doc.id.toUri(), CancellationToken.None);166167assert.strictEqual(fetcher.requests.length, 2, `Unexpected requests: ${JSON.stringify(fetcher.requests, null, 2)}`);168assert.ok(fetcher.requests[0].url.endsWith('/models'), `Unexpected URL: ${fetcher.requests[0].url}`);169assert.ok(fetcher.requests[1].url.endsWith('/chat/completions'), `Unexpected URL: ${fetcher.requests[1].url}`);170171assert(fetcher.requests[1].options.json);172assert(typeof fetcher.requests[1].options.json === 'object');173assert('model' in fetcher.requests[1].options.json);174assert(fetcher.requests[1].options.json.model === 'xtab-test');175176assert(result.result);177178const { range, newText } = result.result;179const offsetRange = OffsetRange.fromTo(range.start, range.endExclusive);180const replace = StringReplacement.replace(offsetRange, newText);181doc.applyEdit(replace.toEdit());182183expect(doc.value.get().value).toMatchInlineSnapshot(`184"class Point3D {185constructor(186private readonly x: number,187private readonly y: number,188private readonly z: number,189) { }190getDistance() {191return Math.sqrt(this.x ** 2 + this.y ** 2);192}193}194195const myPoint = new Point(0, 1);"196`);197198nextEditProvider.handleAcceptance(result);199await new Promise(resolve => setTimeout(resolve, 100)); // wait for async telemetry sending200const event = telemetrySender.events.find(e => e.eventName === 'copilot-nes/provideInlineEdit');201expect(event).toBeDefined();202expect(event!.properties?.acceptance).toBe('accepted');203204nextEditProvider.dispose();205206expect(logTarget.logs.length).toBeGreaterThan(0);207const errorLogs = logTarget.logs.filter(l => l.level === LogLevel.Error);208assert.strictEqual(errorLogs.length, 0, `Unexpected error logs: ${JSON.stringify(errorLogs, null, 2)}`);209});210});211212213