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/githubOrgChatResourcesService.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 } from 'vitest';
8
import type { ExtensionContext } from 'vscode';
9
import { AGENT_FILE_EXTENSION, INSTRUCTION_FILE_EXTENSION, PromptsType } from '../../../../platform/customInstructions/common/promptTypes';
10
import { FileType } from '../../../../platform/filesystem/common/fileTypes';
11
import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService';
12
import { MockAuthenticationService } from '../../../../platform/ignore/node/test/mockAuthenticationService';
13
import { MockGitService } from '../../../../platform/ignore/node/test/mockGitService';
14
import { MockWorkspaceService } from '../../../../platform/ignore/node/test/mockWorkspaceService';
15
import { ILogService } from '../../../../platform/log/common/logService';
16
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
17
import { URI } from '../../../../util/vs/base/common/uri';
18
import { createExtensionUnitTestingServices } from '../../../test/node/services';
19
import { GitHubOrgChatResourcesService } from '../githubOrgChatResourcesService';
20
import { MockOctoKitService } from './mockOctoKitService';
21
22
suite('GitHubOrgChatResourcesService', () => {
23
let disposables: DisposableStore;
24
let mockExtensionContext: Partial<ExtensionContext>;
25
let mockFileSystem: MockFileSystemService;
26
let mockGitService: MockGitService;
27
let mockOctoKitService: MockOctoKitService;
28
let mockWorkspaceService: MockWorkspaceService;
29
let mockAuthService: MockAuthenticationService;
30
let logService: ILogService;
31
let service: GitHubOrgChatResourcesService;
32
33
const storagePath = '/test/storage';
34
const storageUri = URI.file(storagePath);
35
36
beforeEach(() => {
37
disposables = new DisposableStore();
38
39
// Create a simple mock extension context with only globalStorageUri
40
mockExtensionContext = {
41
globalStorageUri: storageUri,
42
};
43
mockFileSystem = new MockFileSystemService();
44
mockGitService = new MockGitService();
45
mockOctoKitService = new MockOctoKitService();
46
mockWorkspaceService = new MockWorkspaceService();
47
mockAuthService = new MockAuthenticationService();
48
49
// Set up testing services to get log service
50
const testingServiceCollection = createExtensionUnitTestingServices(disposables);
51
const accessor = disposables.add(testingServiceCollection.createTestingAccessor());
52
logService = accessor.get(ILogService);
53
});
54
55
afterEach(() => {
56
disposables.dispose();
57
mockOctoKitService?.reset();
58
});
59
60
function createService(): GitHubOrgChatResourcesService {
61
service = new GitHubOrgChatResourcesService(
62
mockAuthService as any,
63
mockExtensionContext as any,
64
mockFileSystem,
65
mockGitService,
66
logService,
67
mockOctoKitService,
68
mockWorkspaceService,
69
);
70
disposables.add(service);
71
return service;
72
}
73
74
suite('getPreferredOrganizationName', () => {
75
76
test('returns organization from workspace repository when available', async () => {
77
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
78
mockGitService.setRepositoryFetchUrls({
79
rootUri: URI.file('/workspace'),
80
remoteFetchUrls: ['https://github.com/myorg/myrepo.git']
81
});
82
mockOctoKitService.setUserOrganizations(['myorg']);
83
84
const service = createService();
85
const orgName = await service.getPreferredOrganizationName();
86
87
assert.equal(orgName, 'myorg');
88
});
89
90
test('returns organization from SSH URL format', async () => {
91
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
92
mockGitService.setRepositoryFetchUrls({
93
rootUri: URI.file('/workspace'),
94
remoteFetchUrls: ['[email protected]:sshorg/myrepo.git']
95
});
96
mockOctoKitService.setUserOrganizations(['sshorg']);
97
98
const service = createService();
99
const orgName = await service.getPreferredOrganizationName();
100
101
assert.equal(orgName, 'sshorg');
102
});
103
104
test('falls back to user organizations when no workspace repo', async () => {
105
mockWorkspaceService.setWorkspaceFolders([]);
106
mockOctoKitService.setUserOrganizations(['fallbackorg', 'anotherorg']);
107
108
const service = createService();
109
const orgName = await service.getPreferredOrganizationName();
110
111
assert.equal(orgName, 'fallbackorg');
112
});
113
114
test('falls back to user organizations when repo has no GitHub remote', async () => {
115
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
116
mockGitService.setRepositoryFetchUrls({
117
rootUri: URI.file('/workspace'),
118
remoteFetchUrls: ['https://gitlab.com/someorg/repo.git']
119
});
120
mockOctoKitService.setUserOrganizations(['fallbackorg']);
121
122
const service = createService();
123
const orgName = await service.getPreferredOrganizationName();
124
125
assert.equal(orgName, 'fallbackorg');
126
});
127
128
test('returns undefined when user has no organizations', async () => {
129
mockWorkspaceService.setWorkspaceFolders([]);
130
mockOctoKitService.setUserOrganizations([]);
131
132
const service = createService();
133
const orgName = await service.getPreferredOrganizationName();
134
135
assert.isUndefined(orgName);
136
});
137
138
test('caches result on subsequent calls', async () => {
139
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
140
mockGitService.setRepositoryFetchUrls({
141
rootUri: URI.file('/workspace'),
142
remoteFetchUrls: ['https://github.com/cachedorg/repo.git']
143
});
144
mockOctoKitService.setUserOrganizations(['cachedorg']);
145
146
const service = createService();
147
148
// First call
149
const orgName1 = await service.getPreferredOrganizationName();
150
assert.equal(orgName1, 'cachedorg');
151
152
// Change the mock - should not affect cached result
153
mockGitService.setRepositoryFetchUrls({
154
rootUri: URI.file('/workspace'),
155
remoteFetchUrls: ['https://github.com/neworg/repo.git']
156
});
157
158
// Second call should return cached value
159
const orgName2 = await service.getPreferredOrganizationName();
160
assert.equal(orgName2, 'cachedorg');
161
});
162
163
test('handles error in getUserOrganizations gracefully', async () => {
164
mockWorkspaceService.setWorkspaceFolders([]);
165
mockOctoKitService.getUserOrganizations = async () => {
166
throw new Error('API Error');
167
};
168
169
const service = createService();
170
const orgName = await service.getPreferredOrganizationName();
171
172
assert.isUndefined(orgName);
173
});
174
175
test('tries multiple remote URLs to find GitHub repo', async () => {
176
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
177
mockGitService.setRepositoryFetchUrls({
178
rootUri: URI.file('/workspace'),
179
remoteFetchUrls: [
180
'https://gitlab.com/notgithub/repo.git',
181
undefined as any, // Skip undefined
182
'https://github.com/foundorg/repo.git'
183
]
184
});
185
mockOctoKitService.setUserOrganizations(['foundorg']);
186
187
const service = createService();
188
const orgName = await service.getPreferredOrganizationName();
189
190
assert.equal(orgName, 'foundorg');
191
});
192
193
test('prefers Copilot sign-in org over arbitrary first org when no workspace repo', async () => {
194
mockWorkspaceService.setWorkspaceFolders([]);
195
mockOctoKitService.setUserOrganizations(['firstorg', 'copilotorg', 'thirdorg']);
196
// Set Copilot token with organization_login_list indicating Copilot access through 'copilotorg'
197
mockAuthService.copilotToken = {
198
organizationLoginList: ['copilotorg'],
199
} as any;
200
201
const service = createService();
202
const orgName = await service.getPreferredOrganizationName();
203
204
assert.equal(orgName, 'copilotorg');
205
});
206
207
test('prefers workspace repo org over Copilot sign-in org', async () => {
208
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
209
mockGitService.setRepositoryFetchUrls({
210
rootUri: URI.file('/workspace'),
211
remoteFetchUrls: ['https://github.com/workspaceorg/repo.git']
212
});
213
mockOctoKitService.setUserOrganizations(['workspaceorg', 'copilotorg']);
214
mockAuthService.copilotToken = {
215
organizationLoginList: ['copilotorg'],
216
} as any;
217
218
const service = createService();
219
const orgName = await service.getPreferredOrganizationName();
220
221
assert.equal(orgName, 'workspaceorg');
222
});
223
224
test('uses Copilot org even when not in paginated user org list', async () => {
225
mockWorkspaceService.setWorkspaceFolders([]);
226
mockOctoKitService.setUserOrganizations(['firstorg', 'secondorg']);
227
// Copilot org may not appear in paginated user org list but is still valid
228
mockAuthService.copilotToken = {
229
organizationLoginList: ['copilotorg'],
230
} as any;
231
232
const service = createService();
233
const orgName = await service.getPreferredOrganizationName();
234
235
// Copilot token orgs are trusted since they represent validated membership
236
assert.equal(orgName, 'copilotorg');
237
});
238
239
test('falls back to first org when no Copilot token available', async () => {
240
mockWorkspaceService.setWorkspaceFolders([]);
241
mockOctoKitService.setUserOrganizations(['firstorg', 'secondorg']);
242
mockAuthService.copilotToken = undefined;
243
244
const service = createService();
245
const orgName = await service.getPreferredOrganizationName();
246
247
assert.equal(orgName, 'firstorg');
248
});
249
250
test('uses first matching Copilot org when multiple are available', async () => {
251
mockWorkspaceService.setWorkspaceFolders([]);
252
mockOctoKitService.setUserOrganizations(['thirdorg', 'secondcopilotorg', 'firstcopilotorg']);
253
mockAuthService.copilotToken = {
254
organizationLoginList: ['firstcopilotorg', 'secondcopilotorg'],
255
} as any;
256
257
const service = createService();
258
const orgName = await service.getPreferredOrganizationName();
259
260
// Should match 'firstcopilotorg' first in the copilot org list iteration
261
assert.equal(orgName, 'firstcopilotorg');
262
});
263
});
264
265
suite.skip('startPolling', () => {
266
267
test('invokes callback immediately with org name', async () => {
268
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
269
mockGitService.setRepositoryFetchUrls({
270
rootUri: URI.file('/workspace'),
271
remoteFetchUrls: ['https://github.com/pollingorg/repo.git']
272
});
273
mockOctoKitService.setUserOrganizations(['pollingorg']);
274
275
const service = createService();
276
277
let capturedOrg: string | undefined;
278
const subscription = service.startPolling(10000, async (orgName) => {
279
capturedOrg = orgName;
280
});
281
disposables.add(subscription);
282
283
// Wait for initial poll
284
await new Promise(resolve => setTimeout(resolve, 50));
285
286
assert.equal(capturedOrg, 'pollingorg');
287
});
288
289
test('does not invoke callback when no organization', async () => {
290
mockWorkspaceService.setWorkspaceFolders([]);
291
mockOctoKitService.setUserOrganizations([]);
292
293
const service = createService();
294
295
let callbackInvoked = false;
296
const subscription = service.startPolling(10000, async () => {
297
callbackInvoked = true;
298
});
299
disposables.add(subscription);
300
301
await new Promise(resolve => setTimeout(resolve, 50));
302
303
assert.isFalse(callbackInvoked);
304
});
305
306
test('stops polling when subscription is disposed', async () => {
307
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
308
mockGitService.setRepositoryFetchUrls({
309
rootUri: URI.file('/workspace'),
310
remoteFetchUrls: ['https://github.com/testorg/repo.git']
311
});
312
313
const service = createService();
314
315
let callCount = 0;
316
const subscription = service.startPolling(50, async () => {
317
callCount++;
318
});
319
320
// Wait for initial poll
321
await new Promise(resolve => setTimeout(resolve, 30));
322
const initialCount = callCount;
323
324
// Dispose subscription
325
subscription.dispose();
326
327
// Wait longer than poll interval
328
await new Promise(resolve => setTimeout(resolve, 100));
329
330
// Call count should not have increased significantly after disposal
331
assert.isAtMost(callCount - initialCount, 1);
332
});
333
334
test('prevents concurrent polling', async () => {
335
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
336
mockGitService.setRepositoryFetchUrls({
337
rootUri: URI.file('/workspace'),
338
remoteFetchUrls: ['https://github.com/concurrent/repo.git']
339
});
340
mockOctoKitService.setUserOrganizations(['concurrent']);
341
342
const service = createService();
343
344
let concurrentCalls = 0;
345
let maxConcurrentCalls = 0;
346
347
const subscription = service.startPolling(10, async () => {
348
concurrentCalls++;
349
maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls);
350
await new Promise(resolve => setTimeout(resolve, 50));
351
concurrentCalls--;
352
});
353
disposables.add(subscription);
354
355
// Wait for multiple poll cycles
356
await new Promise(resolve => setTimeout(resolve, 100));
357
358
// Should never have more than 1 concurrent call
359
assert.equal(maxConcurrentCalls, 1);
360
});
361
362
test('handles callback errors gracefully', async () => {
363
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]);
364
mockGitService.setRepositoryFetchUrls({
365
rootUri: URI.file('/workspace'),
366
remoteFetchUrls: ['https://github.com/errororg/repo.git']
367
});
368
mockOctoKitService.setUserOrganizations(['errororg']);
369
370
const service = createService();
371
372
let callCount = 0;
373
const subscription = service.startPolling(30, async () => {
374
callCount++;
375
if (callCount === 1) {
376
throw new Error('Callback error');
377
}
378
});
379
disposables.add(subscription);
380
381
// Wait for multiple poll cycles
382
await new Promise(resolve => setTimeout(resolve, 100));
383
384
// Should continue polling even after error
385
assert.isAtLeast(callCount, 2);
386
});
387
});
388
389
suite('readCacheFile', () => {
390
391
test('reads instruction file from cache', async () => {
392
const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
393
mockFileSystem.mockFile(cacheUri, '# Custom Instructions');
394
395
const service = createService();
396
const content = await service.readCacheFile(PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}`);
397
398
assert.equal(content, '# Custom Instructions');
399
});
400
401
test('reads agent file from cache', async () => {
402
const cacheUri = URI.file(`${storagePath}/github/testorg/agents/myagent${AGENT_FILE_EXTENSION}`);
403
mockFileSystem.mockFile(cacheUri, '---\nname: My Agent\n---\nPrompt');
404
405
const service = createService();
406
const content = await service.readCacheFile(PromptsType.agent, 'testorg', `myagent${AGENT_FILE_EXTENSION}`);
407
408
assert.equal(content, '---\nname: My Agent\n---\nPrompt');
409
});
410
411
test('returns undefined for missing file', async () => {
412
const service = createService();
413
const content = await service.readCacheFile(PromptsType.instructions, 'testorg', 'nonexistent.instructions.md');
414
415
assert.isUndefined(content);
416
});
417
418
test('sanitizes org name in path', async () => {
419
// dash is preserved, uppercase becomes lowercase
420
const cacheUri = URI.file(`${storagePath}/github/test-org/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
421
mockFileSystem.mockFile(cacheUri, 'Sanitized content');
422
423
const service = createService();
424
const content = await service.readCacheFile(PromptsType.instructions, 'Test-Org', `default${INSTRUCTION_FILE_EXTENSION}`);
425
426
assert.equal(content, 'Sanitized content');
427
});
428
});
429
430
suite('writeCacheFile', () => {
431
432
test('writes instruction file to cache', async () => {
433
const service = createService();
434
435
const result = await service.writeCacheFile(
436
PromptsType.instructions,
437
'testorg',
438
`default${INSTRUCTION_FILE_EXTENSION}`,
439
'# New Instructions'
440
);
441
442
assert.isTrue(result);
443
444
// Verify file was written
445
const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
446
const content = await mockFileSystem.readFile(cacheUri);
447
assert.equal(new TextDecoder().decode(content), '# New Instructions');
448
});
449
450
test('writes agent file to cache', async () => {
451
const service = createService();
452
453
const result = await service.writeCacheFile(
454
PromptsType.agent,
455
'testorg',
456
`myagent${AGENT_FILE_EXTENSION}`,
457
'---\nname: Agent\n---\nPrompt'
458
);
459
460
assert.isTrue(result);
461
462
const cacheUri = URI.file(`${storagePath}/github/testorg/agents/myagent${AGENT_FILE_EXTENSION}`);
463
const content = await mockFileSystem.readFile(cacheUri);
464
assert.equal(new TextDecoder().decode(content), '---\nname: Agent\n---\nPrompt');
465
});
466
467
test('returns false when content unchanged with checkForChanges', async () => {
468
const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
469
mockFileSystem.mockFile(cacheUri, 'Same content');
470
471
const service = createService();
472
473
const result = await service.writeCacheFile(
474
PromptsType.instructions,
475
'testorg',
476
`default${INSTRUCTION_FILE_EXTENSION}`,
477
'Same content',
478
{ checkForChanges: true }
479
);
480
481
assert.isFalse(result);
482
});
483
484
test('returns true when content changed with checkForChanges', async () => {
485
const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
486
mockFileSystem.mockFile(cacheUri, 'Old content');
487
488
const service = createService();
489
490
const result = await service.writeCacheFile(
491
PromptsType.instructions,
492
'testorg',
493
`default${INSTRUCTION_FILE_EXTENSION}`,
494
'New content',
495
{ checkForChanges: true }
496
);
497
498
assert.isTrue(result);
499
});
500
501
test('returns true when file does not exist with checkForChanges', async () => {
502
const service = createService();
503
504
const result = await service.writeCacheFile(
505
PromptsType.instructions,
506
'neworg',
507
`default${INSTRUCTION_FILE_EXTENSION}`,
508
'Content',
509
{ checkForChanges: true }
510
);
511
512
assert.isTrue(result);
513
});
514
515
test('returns true when file size differs with checkForChanges', async () => {
516
const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
517
mockFileSystem.mockFile(cacheUri, 'Short');
518
519
const service = createService();
520
521
const result = await service.writeCacheFile(
522
PromptsType.instructions,
523
'testorg',
524
`default${INSTRUCTION_FILE_EXTENSION}`,
525
'Much longer content that differs in size',
526
{ checkForChanges: true }
527
);
528
529
assert.isTrue(result);
530
});
531
532
test('creates directory structure if not exists', async () => {
533
const service = createService();
534
535
await service.writeCacheFile(
536
PromptsType.agent,
537
'neworg',
538
`agent${AGENT_FILE_EXTENSION}`,
539
'Content'
540
);
541
542
const cacheUri = URI.file(`${storagePath}/github/neworg/agents/agent${AGENT_FILE_EXTENSION}`);
543
const content = await mockFileSystem.readFile(cacheUri);
544
assert.equal(new TextDecoder().decode(content), 'Content');
545
});
546
547
test('sanitizes org name before writing', async () => {
548
const service = createService();
549
550
await service.writeCacheFile(
551
PromptsType.instructions,
552
'My-Org!@#',
553
`default${INSTRUCTION_FILE_EXTENSION}`,
554
'Content'
555
);
556
557
// dash is preserved, special chars become underscore, uppercase becomes lowercase
558
const cacheUri = URI.file(`${storagePath}/github/my-org___/instructions/default${INSTRUCTION_FILE_EXTENSION}`);
559
const content = await mockFileSystem.readFile(cacheUri);
560
assert.equal(new TextDecoder().decode(content), 'Content');
561
});
562
});
563
564
suite('clearCache', () => {
565
566
test('deletes all instruction files for organization', async () => {
567
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
568
mockFileSystem.mockDirectory(cacheDir, [
569
[`file1${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
570
[`file2${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
571
]);
572
mockFileSystem.mockFile(URI.joinPath(cacheDir, `file1${INSTRUCTION_FILE_EXTENSION}`), 'Content 1');
573
mockFileSystem.mockFile(URI.joinPath(cacheDir, `file2${INSTRUCTION_FILE_EXTENSION}`), 'Content 2');
574
575
const service = createService();
576
await service.clearCache(PromptsType.instructions, 'testorg');
577
578
// Files should be deleted
579
let file1Exists = true;
580
let file2Exists = true;
581
try {
582
await mockFileSystem.readFile(URI.joinPath(cacheDir, `file1${INSTRUCTION_FILE_EXTENSION}`));
583
} catch {
584
file1Exists = false;
585
}
586
try {
587
await mockFileSystem.readFile(URI.joinPath(cacheDir, `file2${INSTRUCTION_FILE_EXTENSION}`));
588
} catch {
589
file2Exists = false;
590
}
591
592
assert.isFalse(file1Exists);
593
assert.isFalse(file2Exists);
594
});
595
596
test('excludes specified files from deletion', async () => {
597
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
598
mockFileSystem.mockDirectory(cacheDir, [
599
[`keep${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
600
[`delete${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
601
]);
602
mockFileSystem.mockFile(URI.joinPath(cacheDir, `keep${INSTRUCTION_FILE_EXTENSION}`), 'Keep this');
603
mockFileSystem.mockFile(URI.joinPath(cacheDir, `delete${INSTRUCTION_FILE_EXTENSION}`), 'Delete this');
604
605
const service = createService();
606
await service.clearCache(PromptsType.instructions, 'testorg', new Set([`keep${INSTRUCTION_FILE_EXTENSION}`]));
607
608
// Kept file should still exist
609
const keepContent = await mockFileSystem.readFile(URI.joinPath(cacheDir, `keep${INSTRUCTION_FILE_EXTENSION}`));
610
assert.equal(new TextDecoder().decode(keepContent), 'Keep this');
611
612
// Deleted file should not exist
613
let deleteExists = true;
614
try {
615
await mockFileSystem.readFile(URI.joinPath(cacheDir, `delete${INSTRUCTION_FILE_EXTENSION}`));
616
} catch {
617
deleteExists = false;
618
}
619
assert.isFalse(deleteExists);
620
});
621
622
test('skips non-matching file extensions', async () => {
623
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
624
mockFileSystem.mockDirectory(cacheDir, [
625
[`valid${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
626
['invalid.txt', FileType.File],
627
]);
628
mockFileSystem.mockFile(URI.joinPath(cacheDir, `valid${INSTRUCTION_FILE_EXTENSION}`), 'Valid');
629
mockFileSystem.mockFile(URI.joinPath(cacheDir, 'invalid.txt'), 'Invalid');
630
631
const service = createService();
632
await service.clearCache(PromptsType.instructions, 'testorg');
633
634
// Valid file should be deleted
635
let validExists = true;
636
try {
637
await mockFileSystem.readFile(URI.joinPath(cacheDir, `valid${INSTRUCTION_FILE_EXTENSION}`));
638
} catch {
639
validExists = false;
640
}
641
assert.isFalse(validExists);
642
643
// Invalid file should still exist
644
const invalidContent = await mockFileSystem.readFile(URI.joinPath(cacheDir, 'invalid.txt'));
645
assert.equal(new TextDecoder().decode(invalidContent), 'Invalid');
646
});
647
648
test('handles non-existent cache directory gracefully', async () => {
649
const service = createService();
650
651
// Should not throw
652
await service.clearCache(PromptsType.instructions, 'nonexistentorg');
653
});
654
655
test('skips directories in cache folder', async () => {
656
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
657
mockFileSystem.mockDirectory(cacheDir, [
658
[`file${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
659
['subfolder', FileType.Directory],
660
]);
661
mockFileSystem.mockFile(URI.joinPath(cacheDir, `file${INSTRUCTION_FILE_EXTENSION}`), 'Content');
662
mockFileSystem.mockDirectory(URI.joinPath(cacheDir, 'subfolder'), []);
663
664
const service = createService();
665
await service.clearCache(PromptsType.instructions, 'testorg');
666
667
// Directory should still exist
668
const dirStat = await mockFileSystem.stat(URI.joinPath(cacheDir, 'subfolder'));
669
assert.ok(dirStat);
670
});
671
});
672
673
suite('listCachedFiles', () => {
674
675
test('lists all instruction files for organization', async () => {
676
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
677
mockFileSystem.mockDirectory(cacheDir, [
678
[`file1${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
679
[`file2${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
680
]);
681
682
const service = createService();
683
const files = await service.listCachedFiles(PromptsType.instructions, 'testorg');
684
685
assert.equal(files.length, 2);
686
const fileNames = files.map(f => f.uri.path.split('/').pop());
687
assert.include(fileNames, `file1${INSTRUCTION_FILE_EXTENSION}`);
688
assert.include(fileNames, `file2${INSTRUCTION_FILE_EXTENSION}`);
689
});
690
691
test('lists all agent files for organization', async () => {
692
const cacheDir = URI.file(`${storagePath}/github/testorg/agents`);
693
mockFileSystem.mockDirectory(cacheDir, [
694
[`agent1${AGENT_FILE_EXTENSION}`, FileType.File],
695
[`agent2${AGENT_FILE_EXTENSION}`, FileType.File],
696
]);
697
698
const service = createService();
699
const files = await service.listCachedFiles(PromptsType.agent, 'testorg');
700
701
assert.equal(files.length, 2);
702
const fileNames = files.map(f => f.uri.path.split('/').pop());
703
assert.include(fileNames, `agent1${AGENT_FILE_EXTENSION}`);
704
assert.include(fileNames, `agent2${AGENT_FILE_EXTENSION}`);
705
});
706
707
test('returns empty array for non-existent directory', async () => {
708
const service = createService();
709
const files = await service.listCachedFiles(PromptsType.instructions, 'nonexistent');
710
711
assert.deepEqual(files, []);
712
});
713
714
test('filters out non-matching file extensions', async () => {
715
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
716
mockFileSystem.mockDirectory(cacheDir, [
717
[`valid${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
718
['invalid.txt', FileType.File],
719
['readme.md', FileType.File],
720
]);
721
722
const service = createService();
723
const files = await service.listCachedFiles(PromptsType.instructions, 'testorg');
724
725
assert.equal(files.length, 1);
726
assert.ok(files[0].uri.path.endsWith(INSTRUCTION_FILE_EXTENSION));
727
});
728
729
test('filters out directories', async () => {
730
const cacheDir = URI.file(`${storagePath}/github/testorg/agents`);
731
mockFileSystem.mockDirectory(cacheDir, [
732
[`agent${AGENT_FILE_EXTENSION}`, FileType.File],
733
['subfolder', FileType.Directory],
734
]);
735
736
const service = createService();
737
const files = await service.listCachedFiles(PromptsType.agent, 'testorg');
738
739
assert.equal(files.length, 1);
740
assert.ok(files[0].uri.path.endsWith(AGENT_FILE_EXTENSION));
741
});
742
743
test('returns correct URI structure for files', async () => {
744
const cacheDir = URI.file(`${storagePath}/github/myorg/instructions`);
745
mockFileSystem.mockDirectory(cacheDir, [
746
[`custom${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
747
]);
748
749
const service = createService();
750
const files = await service.listCachedFiles(PromptsType.instructions, 'myorg');
751
752
assert.equal(files.length, 1);
753
assert.ok(files[0].uri.path.includes('/github/'));
754
assert.ok(files[0].uri.path.includes('/myorg/'));
755
assert.ok(files[0].uri.path.includes('/instructions/'));
756
});
757
});
758
759
suite('workspace folder change handling', () => {
760
761
test('invalidates org cache when workspace folders change', async () => {
762
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace1')]);
763
mockGitService.setRepositoryFetchUrls({
764
rootUri: URI.file('/workspace1'),
765
remoteFetchUrls: ['https://github.com/org1/repo.git']
766
});
767
mockOctoKitService.setUserOrganizations(['org1', 'org2']);
768
769
const service = createService();
770
771
// Get initial org name
772
const orgName1 = await service.getPreferredOrganizationName();
773
assert.equal(orgName1, 'org1');
774
775
// Simulate workspace folder change by updating mocks
776
mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace2')]);
777
mockGitService.setRepositoryFetchUrls({
778
rootUri: URI.file('/workspace2'),
779
remoteFetchUrls: ['https://github.com/org2/repo.git']
780
});
781
782
// The cache should be cleared on workspace change event
783
// Since we can't easily fire the event, we verify the subscription is set up
784
// by checking that disposal works
785
service.dispose();
786
});
787
});
788
789
suite('getCacheSubdirectory helper', () => {
790
791
test('uses instructions subdirectory for instructions type', async () => {
792
const service = createService();
793
794
await service.writeCacheFile(
795
PromptsType.instructions,
796
'testorg',
797
`file${INSTRUCTION_FILE_EXTENSION}`,
798
'Content'
799
);
800
801
const files = await service.listCachedFiles(PromptsType.instructions, 'testorg');
802
assert.ok(files[0].uri.path.includes('/instructions/'));
803
});
804
805
test('uses agents subdirectory for agent type', async () => {
806
const service = createService();
807
808
await service.writeCacheFile(
809
PromptsType.agent,
810
'testorg',
811
`file${AGENT_FILE_EXTENSION}`,
812
'Content'
813
);
814
815
const files = await service.listCachedFiles(PromptsType.agent, 'testorg');
816
assert.ok(files[0].uri.path.includes('/agents/'));
817
});
818
});
819
820
suite('file validation', () => {
821
822
test('validates instruction file extension', async () => {
823
const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`);
824
mockFileSystem.mockDirectory(cacheDir, [
825
[`valid${INSTRUCTION_FILE_EXTENSION}`, FileType.File],
826
['valid.agent.md', FileType.File], // Wrong extension for instructions
827
]);
828
829
const service = createService();
830
const files = await service.listCachedFiles(PromptsType.instructions, 'testorg');
831
832
assert.equal(files.length, 1);
833
assert.ok(files[0].uri.path.endsWith(INSTRUCTION_FILE_EXTENSION));
834
});
835
836
test('validates agent file extension', async () => {
837
const cacheDir = URI.file(`${storagePath}/github/testorg/agents`);
838
mockFileSystem.mockDirectory(cacheDir, [
839
[`valid${AGENT_FILE_EXTENSION}`, FileType.File],
840
['valid.instructions.md', FileType.File], // Wrong extension for agents
841
]);
842
843
const service = createService();
844
const files = await service.listCachedFiles(PromptsType.agent, 'testorg');
845
846
assert.equal(files.length, 1);
847
assert.ok(files[0].uri.path.endsWith(AGENT_FILE_EXTENSION));
848
});
849
});
850
});
851
852