Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions chat-lib/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion chat-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@microsoft/tiktokenizer": "^1.0.10",
"@vscode/copilot-api": "^0.1.12",
"@vscode/copilot-api": "^0.1.13",
"@vscode/l10n": "^0.0.18",
"@vscode/prompt-tsx": "^0.4.0-alpha.5",
"jsonc-parser": "^3.3.1",
Expand Down
22 changes: 22 additions & 0 deletions chat-lib/test/getInlineCompletions.reply.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
data: {"choices":[{"index":0,"finish_reason":null}]}

data: {"choices":[{"text":" ","index":0,"finish_reason":null}]}

data: {"choices":[{"text":" console","index":0,"finish_reason":null}]}

data: {"choices":[{"text":".log","index":0,"finish_reason":null}]}

data: {"choices":[{"text":"(\"","index":0,"finish_reason":null}]}

data: {"choices":[{"text":"Hello","index":0,"finish_reason":null}]}

data: {"choices":[{"text":",","index":0,"finish_reason":null}]}

data: {"choices":[{"text":" World","index":0,"finish_reason":null}]}

data: {"choices":[{"text":"!\");","index":0,"finish_reason":null}]}

data: {"choices":[{"index":0,"finish_reason":"stop"}]}

data: [DONE]

241 changes: 241 additions & 0 deletions chat-lib/test/getInlineCompletions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// Load env
import * as dotenv from 'dotenv';
dotenv.config({ path: '../.env' });

import { CAPIClient } from '@vscode/copilot-api';
import { readFile } from 'fs/promises';
import { join } from 'path';
import * as stream from 'stream';
import { assert, describe, expect, it } from 'vitest';
import type { AuthenticationGetSessionOptions, AuthenticationSession, LanguageModelChat } from 'vscode';
import { ResultType } from '../src/_internal/extension/completions-core/vscode-node/lib/src/ghostText/ghostText';
import { createTextDocument } from '../src/_internal/extension/completions-core/vscode-node/lib/src/test/textDocument';
import { TextDocumentIdentifier } from '../src/_internal/extension/completions-core/vscode-node/lib/src/textDocument';
import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '../src/_internal/extension/completions-core/vscode-node/lib/src/textDocumentManager';
import { CopilotToken, TokenEnvelope } from '../src/_internal/platform/authentication/common/copilotToken';
import { ChatEndpointFamily, EmbeddingsEndpointFamily } from '../src/_internal/platform/endpoint/common/endpointProvider';
import { MutableObservableWorkspace } from '../src/_internal/platform/inlineEdits/common/observableWorkspace';
import { FetchOptions, IAbortController, IHeaders, Response } from '../src/_internal/platform/networking/common/fetcherService';
import { IChatEndpoint, IEmbeddingsEndpoint, IFetcher } from '../src/_internal/platform/networking/common/networking';
import { Emitter, Event } from '../src/_internal/util/vs/base/common/event';
import { Disposable } from '../src/_internal/util/vs/base/common/lifecycle';
import { URI } from '../src/_internal/util/vs/base/common/uri';
import { ChatRequest } from '../src/_internal/vscodeTypes';
import { createInlineCompletionsProvider, IActionItem, IAuthenticationService, ICAPIClientService, ICompletionsStatusChangedEvent, ICompletionsTextDocumentManager, IEndpointProvider, ILogTarget, ITelemetrySender, LogLevel } from '../src/main';

class TestFetcher implements IFetcher {
constructor(private readonly responses: Record<string, string>) { }

getUserAgentLibrary(): string {
return 'TestFetcher'; // matches the naming convention inside of completions
}

async fetch(url: string, options: FetchOptions): Promise<Response> {
const uri = URI.parse(url);
const responseText = this.responses[uri.path];

const headers = new class implements IHeaders {
get(name: string): string | null {
return null;
}
*[Symbol.iterator](): Iterator<[string, string]> {
// Empty headers for test
}
};

const found = typeof responseText === 'string';
return new Response(
found ? 200 : 404,
found ? 'OK' : 'Not Found',
headers,
async () => responseText || '',
async () => JSON.parse(responseText || ''),
async () => stream.Readable.from([responseText || ''])
);
}

async disconnectAll(): Promise<unknown> {
return Promise.resolve();
}

makeAbortController(): IAbortController {
return new AbortController();
}

isAbortError(e: any): boolean {
return e && e.name === 'AbortError';
}

isInternetDisconnectedError(e: any): boolean {
return false;
}

isFetcherError(e: any): boolean {
return false;
}

getUserMessageForFetcherError(err: any): string {
return `Test fetcher error: ${err.message}`;
}
}

function createTestCopilotToken(envelope?: Partial<Omit<TokenEnvelope, 'expires_at'>>): CopilotToken {
const REFRESH_BUFFER_SECONDS = 60;
const expires_at = Date.now() + ((envelope?.refresh_in ?? 0) + REFRESH_BUFFER_SECONDS) * 1000;
return new CopilotToken({
token: `test token ${Math.ceil(Math.random() * 100)}`,
refresh_in: 0,
expires_at,
username: 'testuser',
isVscodeTeamMember: false,
copilot_plan: 'testsku',
...envelope
});
}

class TestAuthService extends Disposable implements IAuthenticationService {
readonly _serviceBrand: undefined;
readonly isMinimalMode = true;
readonly anyGitHubSession = undefined;
readonly permissiveGitHubSession = undefined;
readonly copilotToken = createTestCopilotToken();
speculativeDecodingEndpointToken: string | undefined;

private readonly _onDidAuthenticationChange = this._register(new Emitter<void>());
readonly onDidAuthenticationChange: Event<void> = this._onDidAuthenticationChange.event;

private readonly _onDidAccessTokenChange = this._register(new Emitter<void>());
readonly onDidAccessTokenChange = this._onDidAccessTokenChange.event;

private readonly _onDidAdoAuthenticationChange = this._register(new Emitter<void>());
readonly onDidAdoAuthenticationChange = this._onDidAdoAuthenticationChange.event;

async getAnyGitHubSession(options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
return undefined;
}

async getPermissiveGitHubSession(options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
return undefined;
}

async getCopilotToken(force?: boolean): Promise<CopilotToken> {
return this.copilotToken;
}

resetCopilotToken(httpError?: number): void { }

async getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise<string | undefined> {
return undefined;
}
}

class TestTelemetrySender implements ITelemetrySender {
events: { eventName: string; properties?: Record<string, string | undefined>; measurements?: Record<string, number | undefined> }[] = [];
sendTelemetryEvent(eventName: string, properties?: Record<string, string | undefined>, measurements?: Record<string, number | undefined>): void {
this.events.push({ eventName, properties, measurements });
}
}

class TestEndpointProvider implements IEndpointProvider {
readonly _serviceBrand: undefined;

async getAllCompletionModels(forceRefresh?: boolean) {
return [];
}

async getAllChatEndpoints() {
return [];
}

async getChatEndpoint(requestOrFamily: LanguageModelChat | ChatRequest | ChatEndpointFamily): Promise<IChatEndpoint> {
throw new Error('Method not implemented.');
}

async getEmbeddingsEndpoint(family?: EmbeddingsEndpointFamily): Promise<IEmbeddingsEndpoint> {
throw new Error('Method not implemented.');
}
}

class TestCAPIClientService extends CAPIClient implements ICAPIClientService {
readonly _serviceBrand: undefined;
constructor() {
super({} as any, undefined, undefined as any /* IFetcherService */, '-');
}
}

class TestDocumentManager extends Disposable implements ICompletionsTextDocumentManager {
private readonly _onDidChangeTextDocument = this._register(new Emitter<TextDocumentChangeEvent>());
readonly onDidChangeTextDocument = this._onDidChangeTextDocument.event;

private readonly _onDidOpenTextDocument = this._register(new Emitter<TextDocumentOpenEvent>());
readonly onDidOpenTextDocument = this._onDidOpenTextDocument.event;

private readonly _onDidCloseTextDocument = this._register(new Emitter<TextDocumentCloseEvent>());
readonly onDidCloseTextDocument = this._onDidCloseTextDocument.event;

private readonly _onDidFocusTextDocument = this._register(new Emitter<TextDocumentFocusedEvent>());
readonly onDidFocusTextDocument = this._onDidFocusTextDocument.event;

private readonly _onDidChangeWorkspaceFolders = this._register(new Emitter<WorkspaceFoldersChangeEvent>());
readonly onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event;

getTextDocumentsUnsafe() {
return [];
}

findNotebook(doc: TextDocumentIdentifier) {
return undefined;
}

getWorkspaceFolders() {
return [];
}
}

class NullLogTarget implements ILogTarget {
logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void { }
}

describe('getInlineCompletions', () => {
it('should return completions for a document and position', async () => {
const provider = createInlineCompletionsProvider({
fetcher: new TestFetcher({ '/v1/engines/gpt-4o-copilot/completions': await readFile(join(__dirname, 'getInlineCompletions.reply.txt'), 'utf8') }),
authService: new TestAuthService(),
telemetrySender: new TestTelemetrySender(),
logTarget: new NullLogTarget(),
isRunningInTest: true,
contextProviderMatch: async () => 0,
statusHandler: new class { didChange(_: ICompletionsStatusChangedEvent) { } },
documentManager: new TestDocumentManager(),
workspace: new MutableObservableWorkspace(),
urlOpener: new class {
async open(_url: string) { }
},
editorInfo: { name: 'test-editor', version: '1.0.0' },
editorPluginInfo: { name: 'test-plugin', version: '1.0.0' },
relatedPluginInfo: [],
editorSession: {
sessionId: 'test-session-id',
machineId: 'test-machine-id',
},
notificationSender: new class {
async showWarningMessage(_message: string, ..._items: IActionItem[]) { return undefined; }
},
endpointProvider: new TestEndpointProvider(),
capiClientService: new TestCAPIClientService(),
});
const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n}\n');

const result = await provider.getInlineCompletions(doc, { line: 1, character: 0 });

assert(result);
expect(result.length).toBe(1);
expect(result[0].resultType).toBe(ResultType.Async);
expect(result[0].displayText).toBe(' console.log("Hello, World!");');
});
});
3 changes: 2 additions & 1 deletion chat-lib/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"declarationMap": true,
"types": [
"node"
]
],
"paths": {}
},
"include": [
"src",
Expand Down
1 change: 0 additions & 1 deletion chat-lib/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import { loadEnv } from 'vite';
import { defineConfig } from 'vitest/config';

Expand Down
6 changes: 6 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -366,5 +366,11 @@ export default tseslint.config(
'local/no-unlayered-files': 'off',
'no-restricted-imports': 'off'
}
},
{
files: ['./src/lib/node/chatLibMain.ts'],
rules: {
'import/no-restricted-paths': 'off'
}
}
);
Loading
Loading