Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/lib/vscode-node/test/nesProvider.spec.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// Load env
7
import * as dotenv from 'dotenv';
8
dotenv.config({ path: '../.env' });
9
10
import { promises as fs } from 'fs';
11
import { outdent } from 'outdent';
12
import * as path from 'path';
13
import { assert, describe, expect, it } from 'vitest';
14
import { CopilotToken, createTestExtendedTokenInfo } from '../../../platform/authentication/common/copilotToken';
15
import { ICopilotTokenManager } from '../../../platform/authentication/common/copilotTokenManager';
16
import { DocumentId } from '../../../platform/inlineEdits/common/dataTypes/documentId';
17
import { MutableObservableWorkspace } from '../../../platform/inlineEdits/common/observableWorkspace';
18
import { FetchOptions, IAbortController, IHeaders, PaginationOptions, Response } from '../../../platform/networking/common/fetcherService';
19
import { IFetcher } from '../../../platform/networking/common/networking';
20
import { NullTerminalService } from '../../../platform/terminal/common/terminalService';
21
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
22
import { Emitter } from '../../../util/vs/base/common/event';
23
import { URI } from '../../../util/vs/base/common/uri';
24
import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
25
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
26
import { createNESProvider, ILogTarget, ITelemetrySender, LogLevel } from '../../node/chatLibMain';
27
28
29
class TestFetcher implements IFetcher {
30
31
requests: { url: string; options: FetchOptions }[] = [];
32
33
constructor(private readonly responses: Record<string, string>) { }
34
35
getUserAgentLibrary(): string {
36
return 'test-fetcher';
37
}
38
39
async fetch(url: string, options: FetchOptions): Promise<Response> {
40
this.requests.push({ url, options });
41
const uri = URI.parse(url);
42
const responseText = this.responses[uri.path];
43
44
const headers = new class implements IHeaders {
45
get(name: string): string | null {
46
return null;
47
}
48
*[Symbol.iterator](): Iterator<[string, string]> {
49
// Empty headers for test
50
}
51
};
52
53
const found = typeof responseText === 'string';
54
const text = responseText || '';
55
return Response.fromText(
56
found ? 200 : 404,
57
found ? 'OK' : 'Not Found',
58
headers,
59
text,
60
'node-http'
61
);
62
}
63
64
fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {
65
throw new Error('Method not implemented.');
66
}
67
68
async disconnectAll(): Promise<unknown> {
69
return Promise.resolve();
70
}
71
72
makeAbortController(): IAbortController {
73
return new AbortController();
74
}
75
76
isAbortError(e: any): boolean {
77
return e && e.name === 'AbortError';
78
}
79
80
isInternetDisconnectedError(e: any): boolean {
81
return false;
82
}
83
84
isFetcherError(e: any): boolean {
85
return false;
86
}
87
88
isNetworkProcessCrashedError(e: any): boolean {
89
return false;
90
}
91
92
getUserMessageForFetcherError(err: any): string {
93
return `Test fetcher error: ${err.message}`;
94
}
95
}
96
97
class TestCopilotTokenManager implements ICopilotTokenManager {
98
_serviceBrand: undefined;
99
100
onDidCopilotTokenRefresh = new Emitter<void>().event;
101
102
async getCopilotToken(force?: boolean): Promise<CopilotToken> {
103
return new CopilotToken(createTestExtendedTokenInfo({ token: 'fixedToken' }));
104
}
105
106
resetCopilotToken(httpError?: number): void {
107
// nothing
108
}
109
}
110
111
class TestTelemetrySender implements ITelemetrySender {
112
events: { eventName: string; properties?: Record<string, string | undefined>; measurements?: Record<string, number | undefined> }[] = [];
113
sendTelemetryEvent(eventName: string, properties?: Record<string, string | undefined>, measurements?: Record<string, number | undefined>): void {
114
this.events.push({ eventName, properties, measurements });
115
}
116
}
117
118
class TestLogTarget implements ILogTarget {
119
logs: { level: LogLevel; message: string; metadata?: any }[] = [];
120
logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void {
121
this.logs.push({ level, message: metadataStr, metadata: extra });
122
console.log(`[${LogLevel[level]}]${metadataStr}`, ...extra);
123
}
124
}
125
126
describe('NESProvider Facade', () => {
127
it('should handle getNextEdit call with a document URI', async () => {
128
const workspace = new MutableObservableWorkspace();
129
const doc = workspace.addDocument({
130
id: DocumentId.create(URI.file('/test/test.ts').toString()),
131
initialValue: outdent`
132
class Point {
133
constructor(
134
private readonly x: number,
135
private readonly y: number,
136
) { }
137
getDistance() {
138
return Math.sqrt(this.x ** 2 + this.y ** 2);
139
}
140
}
141
142
const myPoint = new Point(0, 1);`.trimStart()
143
});
144
doc.setSelection([new OffsetRange(1, 1)], undefined);
145
const telemetrySender = new TestTelemetrySender();
146
const terminalService = new NullTerminalService();
147
const logTarget = new TestLogTarget();
148
const fetcher = new TestFetcher({
149
'/models': JSON.stringify({ models: [] }),
150
'/chat/completions': await fs.readFile(path.join(__dirname, 'nesProvider.reply.txt'), 'utf8'),
151
});
152
const nextEditProvider = createNESProvider({
153
workspace,
154
fetcher,
155
copilotTokenManager: new TestCopilotTokenManager(),
156
telemetrySender,
157
terminalService,
158
logTarget,
159
});
160
nextEditProvider.updateTreatmentVariables({
161
'config.github.copilot.chat.advanced.inlineEdits.xtabProvider.defaultModelConfigurationString': '{ "modelName": "xtab-test", "promptingStrategy": "copilotNesXtab", "includeTagsInCurrentFile": false }',
162
});
163
164
doc.applyEdit(StringEdit.insert(11, '3D'));
165
166
const result = await nextEditProvider.getNextEdit(doc.id.toUri(), CancellationToken.None);
167
168
assert.strictEqual(fetcher.requests.length, 2, `Unexpected requests: ${JSON.stringify(fetcher.requests, null, 2)}`);
169
assert.ok(fetcher.requests[0].url.endsWith('/models'), `Unexpected URL: ${fetcher.requests[0].url}`);
170
assert.ok(fetcher.requests[1].url.endsWith('/chat/completions'), `Unexpected URL: ${fetcher.requests[1].url}`);
171
172
assert(fetcher.requests[1].options.json);
173
assert(typeof fetcher.requests[1].options.json === 'object');
174
assert('model' in fetcher.requests[1].options.json);
175
assert(fetcher.requests[1].options.json.model === 'xtab-test');
176
177
assert(result.result);
178
179
const { range, newText } = result.result;
180
const offsetRange = OffsetRange.fromTo(range.start, range.endExclusive);
181
const replace = StringReplacement.replace(offsetRange, newText);
182
doc.applyEdit(replace.toEdit());
183
184
expect(doc.value.get().value).toMatchInlineSnapshot(`
185
"class Point3D {
186
constructor(
187
private readonly x: number,
188
private readonly y: number,
189
private readonly z: number,
190
) { }
191
getDistance() {
192
return Math.sqrt(this.x ** 2 + this.y ** 2);
193
}
194
}
195
196
const myPoint = new Point(0, 1);"
197
`);
198
199
nextEditProvider.handleAcceptance(result);
200
await new Promise(resolve => setTimeout(resolve, 100)); // wait for async telemetry sending
201
const event = telemetrySender.events.find(e => e.eventName === 'copilot-nes/provideInlineEdit');
202
expect(event).toBeDefined();
203
expect(event!.properties?.acceptance).toBe('accepted');
204
205
nextEditProvider.dispose();
206
207
expect(logTarget.logs.length).toBeGreaterThan(0);
208
const errorLogs = logTarget.logs.filter(l => l.level === LogLevel.Error);
209
assert.strictEqual(errorLogs.length, 0, `Unexpected error logs: ${JSON.stringify(errorLogs, null, 2)}`);
210
});
211
});
212
213