Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/agents/vscode-node/test/githubOrgInstructionsProvider.spec.ts
13405 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
import { assert } from 'chai';
7
import { afterEach, beforeEach, suite, test, vi } from 'vitest';
8
import type { ExtensionContext } from 'vscode';
9
import { INSTRUCTION_FILE_EXTENSION, PromptsType } from '../../../../platform/customInstructions/common/promptTypes';
10
import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService';
11
import { MockAuthenticationService } from '../../../../platform/ignore/node/test/mockAuthenticationService';
12
import { MockGitService } from '../../../../platform/ignore/node/test/mockGitService';
13
import { MockWorkspaceService } from '../../../../platform/ignore/node/test/mockWorkspaceService';
14
import { ILogService } from '../../../../platform/log/common/logService';
15
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
16
import { URI } from '../../../../util/vs/base/common/uri';
17
import { createExtensionUnitTestingServices } from '../../../test/node/services';
18
import { GitHubOrgChatResourcesService } from '../githubOrgChatResourcesService';
19
import { GitHubOrgInstructionsProvider } from '../githubOrgInstructionsProvider';
20
import { MockOctoKitService } from './mockOctoKitService';
21
22
suite('GitHubOrgInstructionsProvider', () => {
23
let disposables: DisposableStore;
24
let mockOctoKitService: MockOctoKitService;
25
let mockFileSystem: MockFileSystemService;
26
let mockGitService: MockGitService;
27
let mockWorkspaceService: MockWorkspaceService;
28
let mockExtensionContext: Partial<ExtensionContext>;
29
let mockAuthService: MockAuthenticationService;
30
let accessor: any;
31
let provider: GitHubOrgInstructionsProvider;
32
let resourcesService: GitHubOrgChatResourcesService;
33
34
const storagePath = '/tmp/test-storage';
35
const storageUri = URI.file(storagePath);
36
37
beforeEach(() => {
38
vi.useFakeTimers();
39
disposables = new DisposableStore();
40
41
// Create mocks for real GitHubOrgChatResourcesService
42
mockOctoKitService = new MockOctoKitService();
43
mockFileSystem = new MockFileSystemService();
44
mockGitService = new MockGitService();
45
mockWorkspaceService = new MockWorkspaceService();
46
mockExtensionContext = {
47
globalStorageUri: storageUri,
48
};
49
mockAuthService = new MockAuthenticationService();
50
51
// Default: user is in 'testorg' and workspace belongs to 'testorg'
52
mockOctoKitService.setUserOrganizations(['testorg']);
53
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
54
mockGitService.setRepositoryFetchUrls({
55
rootUri: URI.file('/workspace'),
56
remoteFetchUrls: ['https://github.com/testorg/repo.git']
57
});
58
59
// Set up testing services
60
const testingServiceCollection = createExtensionUnitTestingServices(disposables);
61
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
62
});
63
64
afterEach(() => {
65
vi.useRealTimers();
66
disposables.dispose();
67
mockOctoKitService.reset();
68
});
69
70
function createProvider(): GitHubOrgInstructionsProvider {
71
// Create the real GitHubOrgChatResourcesService with mocked dependencies
72
resourcesService = new GitHubOrgChatResourcesService(
73
mockAuthService as any,
74
mockExtensionContext as any,
75
mockFileSystem,
76
mockGitService,
77
accessor.get(ILogService),
78
mockOctoKitService,
79
mockWorkspaceService,
80
);
81
disposables.add(resourcesService);
82
83
// Create provider with real resources service
84
provider = new GitHubOrgInstructionsProvider(
85
accessor.get(ILogService),
86
mockOctoKitService,
87
resourcesService,
88
);
89
disposables.add(provider);
90
return provider;
91
}
92
93
/**
94
* Advance timers and wait for polling callback to complete.
95
* Uses a small time advance to trigger the initial poll without infinite loops.
96
*/
97
async function waitForPolling(): Promise<void> {
98
await vi.advanceTimersByTimeAsync(10);
99
}
100
101
/**
102
* Helper to pre-populate cache files in mock filesystem.
103
*/
104
function prepopulateCache(orgName: string, files: Map<string, string>): void {
105
const cacheDir = URI.file(`${storagePath}/github/${orgName}/instructions`);
106
const dirEntries: [string, import('../../../../platform/filesystem/common/fileTypes').FileType][] = [];
107
for (const [filename, content] of files) {
108
mockFileSystem.mockFile(URI.joinPath(cacheDir, filename), content);
109
dirEntries.push([filename, 1 /* FileType.File */]);
110
}
111
mockFileSystem.mockDirectory(cacheDir, dirEntries);
112
}
113
114
test('returns empty array when no organization available', async () => {
115
mockOctoKitService.setUserOrganizations([]);
116
mockWorkspaceService.setWorkspaceFolders([]);
117
const provider = createProvider();
118
119
const instructions = await provider.provideInstructions({}, {} as any);
120
121
assert.deepEqual(instructions, []);
122
});
123
124
test('returns cached instructions when available', async () => {
125
const orgId = 'testorg';
126
127
// Pre-populate cache with instructions
128
const instructionContent = '# Custom Instructions\nThese are custom instructions for the organization.';
129
prepopulateCache(orgId, new Map([
130
[`default${INSTRUCTION_FILE_EXTENSION}`, instructionContent]
131
]));
132
133
const provider = createProvider();
134
135
const instructions = await provider.provideInstructions({}, {} as any);
136
137
assert.equal(instructions.length, 1);
138
assert.ok(instructions[0].uri.path.endsWith(`default${INSTRUCTION_FILE_EXTENSION}`));
139
});
140
141
test('returns empty array when cache is empty', async () => {
142
// No cache populated
143
const provider = createProvider();
144
145
const instructions = await provider.provideInstructions({}, {} as any);
146
147
assert.deepEqual(instructions, []);
148
});
149
150
test.skip('pollInstructions writes instructions to cache when found', async () => {
151
const orgId = 'testorg';
152
const instructionContent = '# Organization Instructions\nBe helpful and concise.';
153
154
mockOctoKitService.setOrgInstructions(orgId, instructionContent);
155
156
createProvider();
157
await waitForPolling();
158
159
// Verify the instructions were written to cache
160
const cachedContent = await resourcesService.readCacheFile(
161
PromptsType.instructions,
162
orgId,
163
`default${INSTRUCTION_FILE_EXTENSION}`
164
);
165
166
// The implementation adds applyTo front matter to the cached content
167
const expectedContent = `---\napplyTo: '**'\n---\n${instructionContent}`;
168
assert.equal(cachedContent, expectedContent);
169
});
170
171
test.skip('pollInstructions does nothing when no instructions found', async () => {
172
mockOctoKitService.setOrgInstructions('testorg', undefined);
173
174
createProvider();
175
await waitForPolling();
176
177
// Verify no instructions were written
178
const cachedContent = await resourcesService.readCacheFile(
179
PromptsType.instructions,
180
'testorg',
181
`default${INSTRUCTION_FILE_EXTENSION}`
182
);
183
184
assert.isUndefined(cachedContent);
185
});
186
187
test.skip('fires change event when instructions content changes', async () => {
188
const instructionContent = '# New Instructions\nUpdated content.';
189
190
mockOctoKitService.setOrgInstructions('testorg', instructionContent);
191
192
const provider = createProvider();
193
194
let eventFired = false;
195
provider.onDidChangeInstructions(() => {
196
eventFired = true;
197
});
198
199
await waitForPolling();
200
201
assert.isTrue(eventFired, 'Change event should fire when instructions are updated');
202
});
203
204
test.skip('fires change event on every successful poll with instructions', async () => {
205
// Note: The current implementation does not pass checkForChanges option to writeCacheFile,
206
// so change events fire on every poll even when content is unchanged
207
const instructionContent = '# Stable Instructions\nThis content will not change.';
208
209
mockOctoKitService.setOrgInstructions('testorg', instructionContent);
210
211
// Pre-populate cache with the same content
212
prepopulateCache('testorg', new Map([
213
[`default${INSTRUCTION_FILE_EXTENSION}`, instructionContent]
214
]));
215
216
const provider = createProvider();
217
218
let changeEventCount = 0;
219
provider.onDidChangeInstructions(() => {
220
changeEventCount++;
221
});
222
223
await waitForPolling();
224
225
assert.equal(changeEventCount, 1, 'Change event fires on every successful poll');
226
});
227
228
test.skip('pollInstructions handles API errors gracefully without throwing', async () => {
229
// Make the API throw an error
230
mockOctoKitService.getOrgCustomInstructions = async () => {
231
throw new Error('API Error');
232
};
233
234
createProvider();
235
236
// pollInstructions has internal error handling - errors are logged but not thrown
237
// This is intentional to prevent polling failures from crashing the extension
238
let errorThrown = false;
239
try {
240
await waitForPolling();
241
} catch (e: any) {
242
errorThrown = true;
243
}
244
245
assert.isFalse(errorThrown, 'API errors should be handled internally and not propagate');
246
});
247
248
test('returns instructions from correct organization', async () => {
249
// Pre-populate different orgs with different instructions
250
prepopulateCache('org1', new Map([
251
[`default${INSTRUCTION_FILE_EXTENSION}`, 'Org1 instructions']
252
]));
253
prepopulateCache('org2', new Map([
254
[`default${INSTRUCTION_FILE_EXTENSION}`, 'Org2 instructions']
255
]));
256
257
// Set preferred org to org2 by configuring workspace git remote
258
mockOctoKitService.setUserOrganizations(['org1', 'org2']);
259
mockGitService.setRepositoryFetchUrls({
260
rootUri: URI.file('/workspace'),
261
remoteFetchUrls: ['https://github.com/org2/repo.git']
262
});
263
264
const provider = createProvider();
265
266
const instructions = await provider.provideInstructions({}, {} as any);
267
268
assert.equal(instructions.length, 1);
269
// The URI should contain 'org2', not 'org1'
270
assert.ok(instructions[0].uri.path.includes('org2'));
271
});
272
273
test('handles cache read errors gracefully', async () => {
274
const provider = createProvider();
275
276
// Override readDirectory to throw an error
277
const originalReadDirectory = mockFileSystem.readDirectory.bind(mockFileSystem);
278
mockFileSystem.readDirectory = async () => {
279
throw new Error('Cache read error');
280
};
281
282
// Should not throw, should return empty array
283
const instructions = await provider.provideInstructions({}, {} as any);
284
285
assert.deepEqual(instructions, []);
286
287
// Restore original method
288
mockFileSystem.readDirectory = originalReadDirectory;
289
});
290
291
test('respects cancellation token in provideInstructions', async () => {
292
prepopulateCache('testorg', new Map([
293
[`default${INSTRUCTION_FILE_EXTENSION}`, 'Some instructions']
294
]));
295
296
const provider = createProvider();
297
298
// Create a cancelled token
299
const cancelledToken = {
300
isCancellationRequested: true,
301
onCancellationRequested: () => ({ dispose: () => { } })
302
};
303
304
const instructions = await provider.provideInstructions({}, cancelledToken as any);
305
306
// Should return empty array when cancelled
307
assert.deepEqual(instructions, []);
308
});
309
310
test('uses correct file extension for instruction files', async () => {
311
const instructionContent = '# Test Instructions';
312
313
mockOctoKitService.setOrgInstructions('testorg', instructionContent);
314
315
const provider = createProvider();
316
await waitForPolling();
317
318
// Verify the file was written with the correct extension
319
const cachedContent = await resourcesService.readCacheFile(
320
PromptsType.instructions,
321
'testorg',
322
`default${INSTRUCTION_FILE_EXTENSION}`
323
);
324
325
// The implementation adds applyTo front matter to the cached content
326
const expectedContent = `---\napplyTo: '**'\n---\n${instructionContent}`;
327
assert.equal(cachedContent, expectedContent);
328
329
// Prepopulate so we can list it
330
prepopulateCache('testorg', new Map([
331
[`default${INSTRUCTION_FILE_EXTENSION}`, instructionContent]
332
]));
333
334
const instructions = await provider.provideInstructions({}, {} as any);
335
assert.equal(instructions.length, 1);
336
assert.ok(instructions[0].uri.path.endsWith(INSTRUCTION_FILE_EXTENSION));
337
});
338
339
test('disposes polling subscription when provider is disposed', () => {
340
const provider = createProvider();
341
342
// Should not throw when disposed
343
provider.dispose();
344
345
// Provider should be properly cleaned up
346
assert.ok(true, 'Provider disposed without errors');
347
});
348
349
test('multiple instruction files are returned when present', async () => {
350
// Pre-populate cache with multiple instruction files
351
prepopulateCache('testorg', new Map([
352
[`default${INSTRUCTION_FILE_EXTENSION}`, 'Default instructions'],
353
[`custom${INSTRUCTION_FILE_EXTENSION}`, 'Custom instructions'],
354
[`team${INSTRUCTION_FILE_EXTENSION}`, 'Team instructions'],
355
]));
356
357
const provider = createProvider();
358
359
const instructions = await provider.provideInstructions({}, {} as any);
360
361
assert.equal(instructions.length, 3);
362
});
363
});
364
365