Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/test/repoInfoTelemetry.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 'assert';
7
import { beforeEach, suite, test, vi } from 'vitest';
8
import type { FileSystemWatcher, Uri } from 'vscode';
9
import { CopilotToken, createTestExtendedTokenInfo } from '../../../../platform/authentication/common/copilotToken';
10
import { ICopilotTokenStore } from '../../../../platform/authentication/common/copilotTokenStore';
11
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
12
import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';
13
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
14
import { IGitDiffService } from '../../../../platform/git/common/gitDiffService';
15
import { IGitExtensionService } from '../../../../platform/git/common/gitExtensionService';
16
import { IGitService } from '../../../../platform/git/common/gitService';
17
import { NullGitDiffService } from '../../../../platform/git/common/nullGitDiffService';
18
import { NullGitExtensionService } from '../../../../platform/git/common/nullGitExtensionService';
19
import { ILogService } from '../../../../platform/log/common/logService';
20
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
21
import { createPlatformServices } from '../../../../platform/test/node/services';
22
import { NullWorkspaceFileIndex } from '../../../../platform/workspaceChunkSearch/node/nullWorkspaceFileIndex';
23
import { IWorkspaceFileIndex } from '../../../../platform/workspaceChunkSearch/node/workspaceFileIndex';
24
import { Event } from '../../../../util/vs/base/common/event';
25
import { observableValue } from '../../../../util/vs/base/common/observableInternal/observables/observableValue';
26
import { URI } from '../../../../util/vs/base/common/uri';
27
import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';
28
import { RepoInfoTelemetry } from '../repoInfoTelemetry';
29
30
// Import Status enum - use const enum values directly since vitest doesn't handle .d.ts well
31
const Status = {
32
INDEX_MODIFIED: 0,
33
INDEX_ADDED: 1,
34
INDEX_DELETED: 2,
35
INDEX_RENAMED: 3,
36
INDEX_COPIED: 4,
37
MODIFIED: 5,
38
DELETED: 6,
39
UNTRACKED: 7,
40
IGNORED: 8,
41
INTENT_TO_ADD: 9,
42
INTENT_TO_RENAME: 10,
43
TYPE_CHANGED: 11,
44
ADDED_BY_US: 12,
45
ADDED_BY_THEM: 13,
46
DELETED_BY_US: 14,
47
DELETED_BY_THEM: 15,
48
BOTH_ADDED: 16,
49
BOTH_DELETED: 17,
50
BOTH_MODIFIED: 18
51
} as const;
52
53
suite('RepoInfoTelemetry', () => {
54
let accessor: ReturnType<ReturnType<typeof createPlatformServices>['createTestingAccessor']>;
55
let telemetryService: ITelemetryService;
56
let gitService: IGitService;
57
let gitDiffService: IGitDiffService;
58
let gitExtensionService: IGitExtensionService;
59
let copilotTokenStore: ICopilotTokenStore;
60
let logService: ILogService;
61
let fileSystemService: IFileSystemService;
62
let workspaceFileIndex: IWorkspaceFileIndex;
63
let configurationService: IConfigurationService;
64
let mockWatcher: MockFileSystemWatcher;
65
66
beforeEach(() => {
67
const services = createPlatformServices();
68
// Register extension-level services not in platform services by default
69
services.define(IGitDiffService, new SyncDescriptor(NullGitDiffService));
70
services.define(IGitExtensionService, new NullGitExtensionService());
71
services.define(IWorkspaceFileIndex, new SyncDescriptor(NullWorkspaceFileIndex));
72
73
// Override IGitService with a proper mock that has an observable activeRepository
74
const mockGitService: IGitService = {
75
_serviceBrand: undefined,
76
activeRepository: observableValue('test-git-activeRepo', undefined),
77
onDidOpenRepository: Event.None,
78
onDidCloseRepository: Event.None,
79
onDidFinishInitialization: Event.None,
80
repositories: [],
81
isInitialized: true,
82
initRepository: vi.fn(),
83
openRepository: vi.fn(),
84
getRepository: vi.fn(),
85
getRepository2: vi.fn(),
86
getRecentRepositories: vi.fn(),
87
getRepositoryFetchUrls: vi.fn(),
88
generateRandomBranchName: vi.fn(),
89
initialize: vi.fn(),
90
diffBetweenWithStats: vi.fn(),
91
diffBetweenPatch: vi.fn(),
92
diffWith: vi.fn(),
93
diffIndexWithHEADShortStats: vi.fn(),
94
getMergeBase: vi.fn(),
95
restore: vi.fn(),
96
add: vi.fn(),
97
createWorktree: vi.fn(),
98
deleteWorktree: vi.fn(),
99
migrateChanges: vi.fn(),
100
applyPatch: vi.fn(),
101
commit: vi.fn(),
102
getRefs: vi.fn(),
103
getBranch: vi.fn(),
104
getBranchBase: vi.fn(),
105
isBranchProtected: vi.fn(),
106
exec: vi.fn(),
107
dispose: vi.fn()
108
};
109
services.define(IGitService, mockGitService);
110
111
accessor = services.createTestingAccessor();
112
113
telemetryService = accessor.get(ITelemetryService);
114
gitService = accessor.get(IGitService);
115
gitDiffService = accessor.get(IGitDiffService);
116
gitExtensionService = accessor.get(IGitExtensionService);
117
copilotTokenStore = accessor.get(ICopilotTokenStore);
118
logService = accessor.get(ILogService);
119
fileSystemService = accessor.get(IFileSystemService);
120
workspaceFileIndex = accessor.get(IWorkspaceFileIndex);
121
configurationService = accessor.get(IConfigurationService);
122
123
// Create a new mock watcher for each test
124
mockWatcher = new MockFileSystemWatcher();
125
126
// Mock the file system service to return our mock watcher
127
vi.spyOn(fileSystemService, 'createFileSystemWatcher').mockReturnValue(mockWatcher as any);
128
129
// Properly mock the telemetry methods
130
(telemetryService as any).sendMSFTTelemetryEvent = vi.fn();
131
(telemetryService as any).sendInternalMSFTTelemetryEvent = vi.fn();
132
});
133
134
// ========================================
135
// Basic Telemetry Flow Tests
136
// ========================================
137
138
test('should not send any telemetry for non-internal users', async () => {
139
// Setup: non-internal user
140
const nonInternalToken = new CopilotToken(createTestExtendedTokenInfo({
141
token: 'test-token',
142
sku: 'free_limited_copilot',
143
expires_at: 9999999999,
144
refresh_in: 180000,
145
organization_list: [],
146
isVscodeTeamMember: false,
147
username: 'testUser',
148
copilot_plan: 'unknown',
149
}));
150
copilotTokenStore.copilotToken = nonInternalToken;
151
152
// Setup: mock git service to have a repository
153
mockGitServiceWithRepository();
154
mockGitExtensionWithUpstream('abc123');
155
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
156
157
const repoTelemetry = new RepoInfoTelemetry(
158
'test-message-id',
159
telemetryService,
160
gitService,
161
gitDiffService,
162
gitExtensionService,
163
logService,
164
fileSystemService,
165
workspaceFileIndex,
166
configurationService,
167
copilotTokenStore
168
);
169
170
await repoTelemetry.sendBeginTelemetryIfNeeded();
171
await repoTelemetry.sendEndTelemetry();
172
173
// Assert: no telemetry sent for non-internal users
174
assert.strictEqual((telemetryService.sendMSFTTelemetryEvent as any).mock.calls.length, 0, 'sendMSFTTelemetryEvent should not be called for non-internal users');
175
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0, 'sendInternalMSFTTelemetryEvent should not be called for non-internal users');
176
});
177
178
test('should send telemetry for internal users', async () => {
179
// Setup: internal user
180
setupInternalUser();
181
mockGitServiceWithRepository();
182
mockGitExtensionWithUpstream('abc123');
183
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
184
185
const repoTelemetry = new RepoInfoTelemetry(
186
'test-message-id',
187
telemetryService,
188
gitService,
189
gitDiffService,
190
gitExtensionService,
191
logService,
192
fileSystemService,
193
workspaceFileIndex,
194
configurationService,
195
copilotTokenStore
196
);
197
198
await repoTelemetry.sendBeginTelemetryIfNeeded();
199
200
// Assert: begin telemetry sent
201
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
202
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
203
assert.strictEqual(call[0], 'request.repoInfo');
204
assert.strictEqual(call[1].location, 'begin');
205
assert.strictEqual(call[1].telemetryMessageId, 'test-message-id');
206
// Check measurements parameter exists
207
assert.ok(call[2], 'measurements parameter should be present');
208
assert.strictEqual(typeof call[2].workspaceFileCount, 'number');
209
});
210
211
test('should send begin telemetry only once', async () => {
212
setupInternalUser();
213
mockGitServiceWithRepository();
214
mockGitExtensionWithUpstream('abc123');
215
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
216
217
const repoTelemetry = new RepoInfoTelemetry(
218
'test-message-id',
219
telemetryService,
220
gitService,
221
gitDiffService,
222
gitExtensionService,
223
logService,
224
fileSystemService,
225
workspaceFileIndex,
226
configurationService,
227
copilotTokenStore
228
);
229
230
await repoTelemetry.sendBeginTelemetryIfNeeded();
231
await repoTelemetry.sendBeginTelemetryIfNeeded();
232
await repoTelemetry.sendBeginTelemetryIfNeeded();
233
234
// Assert: only one begin telemetry sent
235
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
236
});
237
238
test('should send end telemetry after begin', async () => {
239
setupInternalUser();
240
mockGitServiceWithRepository();
241
mockGitExtensionWithUpstream('abc123');
242
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
243
244
const repoTelemetry = new RepoInfoTelemetry(
245
'test-message-id',
246
telemetryService,
247
gitService,
248
gitDiffService,
249
gitExtensionService,
250
logService,
251
fileSystemService,
252
workspaceFileIndex,
253
configurationService,
254
copilotTokenStore
255
);
256
257
await repoTelemetry.sendBeginTelemetryIfNeeded();
258
await repoTelemetry.sendEndTelemetry();
259
260
// Assert: both begin and end telemetry sent
261
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);
262
const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
263
const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];
264
265
assert.strictEqual(beginCall[1].location, 'begin');
266
assert.strictEqual(endCall[1].location, 'end');
267
assert.strictEqual(beginCall[1].telemetryMessageId, endCall[1].telemetryMessageId);
268
});
269
270
test('should send end telemetry when begin has success result', async () => {
271
setupInternalUser();
272
mockGitServiceWithRepository();
273
mockGitExtensionWithUpstream('abc123');
274
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
275
276
const repoTelemetry = new RepoInfoTelemetry(
277
'test-message-id',
278
telemetryService,
279
gitService,
280
gitDiffService,
281
gitExtensionService,
282
logService,
283
fileSystemService,
284
workspaceFileIndex,
285
configurationService,
286
copilotTokenStore
287
);
288
289
await repoTelemetry.sendBeginTelemetryIfNeeded();
290
await repoTelemetry.sendEndTelemetry();
291
292
// Assert: both begin and end telemetry sent
293
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);
294
const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
295
const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];
296
assert.strictEqual(beginCall[1].location, 'begin');
297
assert.strictEqual(beginCall[1].result, 'success');
298
assert.strictEqual(endCall[1].location, 'end');
299
assert.strictEqual(endCall[1].result, 'success');
300
});
301
302
test('should send end telemetry when begin has noChanges result', async () => {
303
setupInternalUser();
304
mockGitServiceWithRepository();
305
mockGitExtensionWithUpstream('abc123');
306
307
// Mock: no changes from upstream
308
vi.spyOn(gitService, 'diffWith').mockResolvedValue([]);
309
310
const repoTelemetry = new RepoInfoTelemetry(
311
'test-message-id',
312
telemetryService,
313
gitService,
314
gitDiffService,
315
gitExtensionService,
316
logService,
317
fileSystemService,
318
workspaceFileIndex,
319
configurationService,
320
copilotTokenStore
321
);
322
323
await repoTelemetry.sendBeginTelemetryIfNeeded();
324
await repoTelemetry.sendEndTelemetry();
325
326
// Assert: both begin and end telemetry sent
327
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);
328
const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
329
const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];
330
assert.strictEqual(beginCall[1].location, 'begin');
331
assert.strictEqual(beginCall[1].result, 'noChanges');
332
assert.strictEqual(endCall[1].location, 'end');
333
assert.strictEqual(endCall[1].result, 'noChanges');
334
});
335
336
test('should skip end telemetry when begin has failure result', async () => {
337
setupInternalUser();
338
mockGitServiceWithRepository();
339
mockGitExtensionWithUpstream('abc123');
340
341
// Mock: too many changes (failure result)
342
const manyChanges = Array.from({ length: 101 }, (_, i) => ({
343
uri: URI.file(`/test/repo/file${i}.ts`),
344
originalUri: URI.file(`/test/repo/file${i}.ts`),
345
renameUri: undefined,
346
status: Status.MODIFIED
347
}));
348
vi.spyOn(gitService, 'diffWith').mockResolvedValue(manyChanges as any);
349
350
const repoTelemetry = new RepoInfoTelemetry(
351
'test-message-id',
352
telemetryService,
353
gitService,
354
gitDiffService,
355
gitExtensionService,
356
logService,
357
fileSystemService,
358
workspaceFileIndex,
359
configurationService,
360
copilotTokenStore
361
);
362
363
await repoTelemetry.sendBeginTelemetryIfNeeded();
364
await repoTelemetry.sendEndTelemetry();
365
366
// Assert: only begin telemetry sent, end was skipped
367
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
368
const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
369
assert.strictEqual(beginCall[1].location, 'begin');
370
assert.strictEqual(beginCall[1].result, 'tooManyChanges');
371
});
372
373
// ========================================
374
// Git Repository Detection Tests
375
// ========================================
376
377
test('should not send telemetry when no active repository', async () => {
378
setupInternalUser();
379
380
// Mock: no active repository
381
vi.spyOn(gitService.activeRepository, 'get').mockReturnValue(undefined);
382
383
const repoTelemetry = new RepoInfoTelemetry(
384
'test-message-id',
385
telemetryService,
386
gitService,
387
gitDiffService,
388
gitExtensionService,
389
logService,
390
fileSystemService,
391
workspaceFileIndex,
392
configurationService,
393
copilotTokenStore
394
);
395
396
await repoTelemetry.sendBeginTelemetryIfNeeded();
397
398
// Assert: no telemetry sent
399
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);
400
});
401
402
test('should send telemetry with noChanges result when no changes from upstream', async () => {
403
setupInternalUser();
404
mockGitServiceWithRepository();
405
mockGitExtensionWithUpstream('abc123');
406
407
// Mock: no changes from upstream
408
vi.spyOn(gitService, 'diffWith').mockResolvedValue([]);
409
410
const repoTelemetry = new RepoInfoTelemetry(
411
'test-message-id',
412
telemetryService,
413
gitService,
414
gitDiffService,
415
gitExtensionService,
416
logService,
417
fileSystemService,
418
workspaceFileIndex,
419
configurationService,
420
copilotTokenStore
421
);
422
423
await repoTelemetry.sendBeginTelemetryIfNeeded();
424
425
// Assert: telemetry sent with noChanges result
426
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
427
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
428
assert.strictEqual(call[1].result, 'noChanges');
429
assert.strictEqual(call[1].diffsJSON, undefined);
430
assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');
431
assert.strictEqual(call[1].headCommitHash, 'abc123');
432
});
433
434
test('should not send telemetry when no GitHub or ADO remote', async () => {
435
setupInternalUser();
436
437
// Mock: repository with changes but no GitHub or ADO remote
438
vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({
439
rootUri: URI.file('/test/repo'),
440
changes: {
441
mergeChanges: [],
442
indexChanges: [],
443
workingTree: [],
444
untrackedChanges: []
445
},
446
remotes: [],
447
remoteFetchUrls: [],
448
upstreamRemote: undefined,
449
} as any);
450
451
mockGitExtensionWithUpstream('abc123', 'https://gitlab.com/user/repo.git');
452
453
const repoTelemetry = new RepoInfoTelemetry(
454
'test-message-id',
455
telemetryService,
456
gitService,
457
gitDiffService,
458
gitExtensionService,
459
logService,
460
fileSystemService,
461
workspaceFileIndex,
462
configurationService,
463
copilotTokenStore
464
);
465
466
await repoTelemetry.sendBeginTelemetryIfNeeded();
467
468
// Assert: no telemetry sent
469
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);
470
});
471
472
test('should send telemetry with correct repoType for Azure DevOps repository', async () => {
473
setupInternalUser();
474
475
// Mock: ADO repository
476
vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({
477
rootUri: URI.file('/test/repo'),
478
changes: {
479
mergeChanges: [],
480
indexChanges: [],
481
workingTree: [{
482
uri: URI.file('/test/repo/file.ts'),
483
originalUri: URI.file('/test/repo/file.ts'),
484
renameUri: undefined,
485
status: Status.MODIFIED
486
}],
487
untrackedChanges: []
488
},
489
remotes: ['origin'],
490
remoteFetchUrls: ['https://dev.azure.com/myorg/myproject/_git/myrepo'],
491
upstreamRemote: 'origin',
492
headBranchName: 'main',
493
headCommitHash: 'abc123',
494
upstreamBranchName: 'origin/main',
495
isRebasing: false,
496
} as any);
497
498
mockGitExtensionWithUpstream('abc123def456', 'https://dev.azure.com/myorg/myproject/_git/myrepo');
499
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
500
501
const repoTelemetry = new RepoInfoTelemetry(
502
'test-message-id',
503
telemetryService,
504
gitService,
505
gitDiffService,
506
gitExtensionService,
507
logService,
508
fileSystemService,
509
workspaceFileIndex,
510
configurationService,
511
copilotTokenStore
512
);
513
514
await repoTelemetry.sendBeginTelemetryIfNeeded();
515
516
// Assert: telemetry sent with repoType = 'ado'
517
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
518
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
519
assert.strictEqual(call[0], 'request.repoInfo');
520
assert.strictEqual(call[1].repoType, 'ado');
521
assert.strictEqual(call[1].remoteUrl, 'https://dev.azure.com/myorg/myproject/_git/myrepo');
522
assert.strictEqual(call[1].headCommitHash, 'abc123def456');
523
assert.strictEqual(call[1].result, 'success');
524
});
525
526
test('should normalize remote URL when logging telemetry', async () => {
527
setupInternalUser();
528
529
// Mock: repository with SSH-style URL that needs normalization
530
const sshUrl = '[email protected]:microsoft/vscode.git';
531
vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({
532
rootUri: URI.file('/test/repo'),
533
changes: {
534
mergeChanges: [],
535
indexChanges: [],
536
workingTree: [{
537
uri: URI.file('/test/repo/file.ts'),
538
originalUri: URI.file('/test/repo/file.ts'),
539
renameUri: undefined,
540
status: Status.MODIFIED
541
}],
542
untrackedChanges: []
543
},
544
remotes: ['origin'],
545
remoteFetchUrls: [sshUrl],
546
upstreamRemote: 'origin',
547
headBranchName: 'main',
548
headCommitHash: 'abc123',
549
upstreamBranchName: 'origin/main',
550
isRebasing: false,
551
} as any);
552
553
mockGitExtensionWithUpstream('abc123def456', sshUrl);
554
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
555
556
const repoTelemetry = new RepoInfoTelemetry(
557
'test-message-id',
558
telemetryService,
559
gitService,
560
gitDiffService,
561
gitExtensionService,
562
logService,
563
fileSystemService,
564
workspaceFileIndex,
565
configurationService,
566
copilotTokenStore
567
);
568
569
await repoTelemetry.sendBeginTelemetryIfNeeded();
570
571
// Assert: URL is normalized to HTTPS
572
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
573
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
574
assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');
575
assert.notStrictEqual(call[1].remoteUrl, sshUrl);
576
});
577
578
test('should not send telemetry when no upstream commit', async () => {
579
setupInternalUser();
580
mockGitServiceWithRepository();
581
582
// Mock: no upstream commit
583
mockGitExtensionWithUpstream(undefined);
584
585
const repoTelemetry = new RepoInfoTelemetry(
586
'test-message-id',
587
telemetryService,
588
gitService,
589
gitDiffService,
590
gitExtensionService,
591
logService,
592
fileSystemService,
593
workspaceFileIndex,
594
configurationService,
595
copilotTokenStore
596
);
597
598
await repoTelemetry.sendBeginTelemetryIfNeeded();
599
600
// Assert: no telemetry sent
601
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);
602
});
603
604
test('should send telemetry with valid GitHub repository', async () => {
605
setupInternalUser();
606
mockGitServiceWithRepository();
607
mockGitExtensionWithUpstream('abc123def456');
608
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
609
610
const repoTelemetry = new RepoInfoTelemetry(
611
'test-message-id',
612
telemetryService,
613
gitService,
614
gitDiffService,
615
gitExtensionService,
616
logService,
617
fileSystemService,
618
workspaceFileIndex,
619
configurationService,
620
copilotTokenStore
621
);
622
623
await repoTelemetry.sendBeginTelemetryIfNeeded();
624
625
// Assert: telemetry sent with correct properties
626
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
627
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
628
assert.strictEqual(call[0], 'request.repoInfo');
629
assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');
630
assert.strictEqual(call[1].headCommitHash, 'abc123def456');
631
assert.strictEqual(call[1].result, 'success');
632
});
633
634
// ========================================
635
// File System Watching Tests
636
// ========================================
637
638
test('should detect file creation during diff', async () => {
639
setupInternalUser();
640
mockGitServiceWithRepository();
641
mockGitExtensionWithUpstream('abc123');
642
643
// Mock git diff to trigger file change during execution
644
vi.spyOn(gitService, 'diffWith').mockImplementation(async () => {
645
// Simulate file creation during diff
646
mockWatcher.triggerCreate(URI.file('/test/repo/newfile.ts') as any);
647
648
// Mock a change being returned from diffWith, we don't want to see this in the final telemetry
649
// instead we want to see the 'filesChanged' result due to the file system change
650
return [{
651
uri: URI.file('/test/repo/file.ts'),
652
originalUri: URI.file('/test/repo/file.ts'),
653
renameUri: undefined,
654
status: Status.MODIFIED
655
}] as any;
656
});
657
658
const repoTelemetry = new RepoInfoTelemetry(
659
'test-message-id',
660
telemetryService,
661
gitService,
662
gitDiffService,
663
gitExtensionService,
664
logService,
665
fileSystemService,
666
workspaceFileIndex,
667
configurationService,
668
copilotTokenStore
669
);
670
671
await repoTelemetry.sendBeginTelemetryIfNeeded();
672
673
// Assert: filesChanged result
674
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
675
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
676
assert.strictEqual(call[1].result, 'filesChanged');
677
assert.strictEqual(call[1].diffsJSON, undefined);
678
});
679
680
test('should detect file modification during diff', async () => {
681
setupInternalUser();
682
mockGitServiceWithRepository();
683
mockGitExtensionWithUpstream('abc123');
684
685
// Mock git diff to trigger file change during execution
686
vi.spyOn(gitService, 'diffWith').mockImplementation(async () => {
687
// Simulate file modification during diff
688
mockWatcher.triggerChange(URI.file('/test/repo/file.ts') as any);
689
690
// Mock a change being returned from diffWith, we don't want to see this in the final telemetry
691
// instead we want to see the 'filesChanged' result due to the file system change
692
return [{
693
uri: URI.file('/test/repo/file.ts'),
694
originalUri: URI.file('/test/repo/file.ts'),
695
renameUri: undefined,
696
status: Status.MODIFIED
697
}] as any;
698
});
699
700
const repoTelemetry = new RepoInfoTelemetry(
701
'test-message-id',
702
telemetryService,
703
gitService,
704
gitDiffService,
705
gitExtensionService,
706
logService,
707
fileSystemService,
708
workspaceFileIndex,
709
configurationService,
710
copilotTokenStore
711
);
712
713
await repoTelemetry.sendBeginTelemetryIfNeeded();
714
715
// Assert: filesChanged result
716
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
717
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
718
assert.strictEqual(call[1].result, 'filesChanged');
719
assert.strictEqual(call[1].diffsJSON, undefined);
720
});
721
722
test('should detect file deletion during diff', async () => {
723
setupInternalUser();
724
mockGitServiceWithRepository();
725
mockGitExtensionWithUpstream('abc123');
726
727
// Mock git diff to trigger file change during execution
728
vi.spyOn(gitService, 'diffWith').mockImplementation(async () => {
729
// Simulate file deletion during diff
730
mockWatcher.triggerDelete(URI.file('/test/repo/oldfile.ts') as any);
731
732
// Mock a change being returned from diffWith, we don't want to see this in the final telemetry
733
// instead we want to see the 'filesChanged' result due to the file system change
734
return [{
735
uri: URI.file('/test/repo/file.ts'),
736
originalUri: URI.file('/test/repo/file.ts'),
737
renameUri: undefined,
738
status: Status.MODIFIED
739
}] as any;
740
});
741
742
const repoTelemetry = new RepoInfoTelemetry(
743
'test-message-id',
744
telemetryService,
745
gitService,
746
gitDiffService,
747
gitExtensionService,
748
logService,
749
fileSystemService,
750
workspaceFileIndex,
751
configurationService,
752
copilotTokenStore
753
);
754
755
await repoTelemetry.sendBeginTelemetryIfNeeded();
756
757
// Assert: filesChanged result
758
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
759
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
760
assert.strictEqual(call[1].result, 'filesChanged');
761
assert.strictEqual(call[1].diffsJSON, undefined);
762
});
763
764
test('should detect file change during diff processing', async () => {
765
setupInternalUser();
766
mockGitServiceWithRepository();
767
mockGitExtensionWithUpstream('abc123');
768
769
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
770
uri: URI.file('/test/repo/file.ts'),
771
originalUri: URI.file('/test/repo/file.ts'),
772
renameUri: undefined,
773
status: Status.MODIFIED
774
}] as any);
775
776
// Mock git diff service to trigger file change during processing
777
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockImplementation(async () => {
778
// Simulate file change during diff processing
779
mockWatcher.triggerChange(URI.file('/test/repo/file.ts') as any);
780
return [{
781
uri: URI.file('/test/repo/file.ts'),
782
originalUri: URI.file('/test/repo/file.ts'),
783
renameUri: undefined,
784
status: Status.MODIFIED,
785
diff: 'some diff content'
786
}];
787
});
788
789
const repoTelemetry = new RepoInfoTelemetry(
790
'test-message-id',
791
telemetryService,
792
gitService,
793
gitDiffService,
794
gitExtensionService,
795
logService,
796
fileSystemService,
797
workspaceFileIndex,
798
configurationService,
799
copilotTokenStore
800
);
801
802
await repoTelemetry.sendBeginTelemetryIfNeeded();
803
804
// Assert: filesChanged result
805
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
806
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
807
assert.strictEqual(call[1].result, 'filesChanged');
808
assert.strictEqual(call[1].diffsJSON, undefined);
809
});
810
811
test('should properly dispose file watcher', async () => {
812
setupInternalUser();
813
mockGitServiceWithRepository();
814
mockGitExtensionWithUpstream('abc123');
815
mockGitDiffService([]);
816
817
const repoTelemetry = new RepoInfoTelemetry(
818
'test-message-id',
819
telemetryService,
820
gitService,
821
gitDiffService,
822
gitExtensionService,
823
logService,
824
fileSystemService,
825
workspaceFileIndex,
826
configurationService,
827
copilotTokenStore
828
);
829
830
await repoTelemetry.sendBeginTelemetryIfNeeded();
831
832
// Assert: watcher was disposed
833
assert.strictEqual(mockWatcher.isDisposed, true);
834
});
835
836
// ========================================
837
// VFS / Sparse Checkout Tests
838
// ========================================
839
840
test('should skip with virtualFileSystem result when core.virtualfilesystem is set', async () => {
841
setupInternalUser();
842
mockGitServiceWithRepository();
843
mockGitExtensionWithUpstream('abc123');
844
845
// Override getConfig to return a hook path for core.virtualfilesystem (any non-empty string means VFS is active)
846
const mockApi = gitExtensionService.getExtensionApi();
847
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
848
vi.spyOn(mockRepo, 'getConfig').mockImplementation(async key => {
849
if (key === 'core.virtualfilesystem') {
850
return '/path/to/vfs-hook';
851
}
852
return '';
853
});
854
855
const repoTelemetry = new RepoInfoTelemetry(
856
'test-message-id',
857
telemetryService,
858
gitService,
859
gitDiffService,
860
gitExtensionService,
861
logService,
862
fileSystemService,
863
workspaceFileIndex,
864
configurationService,
865
copilotTokenStore
866
);
867
868
await repoTelemetry.sendBeginTelemetryIfNeeded();
869
870
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
871
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
872
assert.strictEqual(call[1].result, 'virtualFileSystem');
873
assert.strictEqual(call[1].diffsJSON, undefined);
874
875
// Ensure expensive diff operations were never called
876
assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);
877
});
878
879
test('should skip with virtualFileSystem result when core.sparsecheckout is true', async () => {
880
setupInternalUser();
881
mockGitServiceWithRepository();
882
mockGitExtensionWithUpstream('abc123');
883
884
const mockApi = gitExtensionService.getExtensionApi();
885
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
886
vi.spyOn(mockRepo, 'getConfig').mockImplementation(async key => {
887
if (key === 'core.sparsecheckout') {
888
return 'true';
889
}
890
return '';
891
});
892
893
const repoTelemetry = new RepoInfoTelemetry(
894
'test-message-id',
895
telemetryService,
896
gitService,
897
gitDiffService,
898
gitExtensionService,
899
logService,
900
fileSystemService,
901
workspaceFileIndex,
902
configurationService,
903
copilotTokenStore
904
);
905
906
await repoTelemetry.sendBeginTelemetryIfNeeded();
907
908
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
909
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
910
assert.strictEqual(call[1].result, 'virtualFileSystem');
911
assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);
912
});
913
914
test('should skip with virtualFileSystem result when getConfig throws', async () => {
915
setupInternalUser();
916
mockGitServiceWithRepository();
917
mockGitExtensionWithUpstream('abc123');
918
919
const mockApi = gitExtensionService.getExtensionApi();
920
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
921
vi.spyOn(mockRepo, 'getConfig').mockRejectedValue(new Error('git config failed'));
922
923
const repoTelemetry = new RepoInfoTelemetry(
924
'test-message-id',
925
telemetryService,
926
gitService,
927
gitDiffService,
928
gitExtensionService,
929
logService,
930
fileSystemService,
931
workspaceFileIndex,
932
configurationService,
933
copilotTokenStore
934
);
935
936
await repoTelemetry.sendBeginTelemetryIfNeeded();
937
938
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
939
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
940
assert.strictEqual(call[1].result, 'virtualFileSystem');
941
assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);
942
});
943
944
// ========================================
945
// Commit Count Tests
946
// ========================================
947
948
test('should skip with tooManyCommits result when commit count exceeds limit', async () => {
949
setupInternalUser();
950
mockGitServiceWithRepository();
951
mockGitExtensionWithUpstream('abc123');
952
953
const mockApi = gitExtensionService.getExtensionApi();
954
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
955
// Return 30 commits (>= MAX_DIFF_COMMITS)
956
vi.spyOn(mockRepo, 'log').mockResolvedValue(
957
Array.from({ length: 30 }, (_, i) => ({ hash: `commit${i}`, message: `msg${i}` })) as any
958
);
959
960
const repoTelemetry = new RepoInfoTelemetry(
961
'test-message-id',
962
telemetryService,
963
gitService,
964
gitDiffService,
965
gitExtensionService,
966
logService,
967
fileSystemService,
968
workspaceFileIndex,
969
configurationService,
970
copilotTokenStore
971
);
972
973
await repoTelemetry.sendBeginTelemetryIfNeeded();
974
975
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
976
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
977
assert.strictEqual(call[1].result, 'tooManyCommits');
978
assert.strictEqual(call[1].diffsJSON, undefined);
979
assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);
980
});
981
982
test('should proceed normally when commit count is below limit', async () => {
983
setupInternalUser();
984
mockGitServiceWithRepository();
985
mockGitExtensionWithUpstream('abc123');
986
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
987
988
const mockApi = gitExtensionService.getExtensionApi();
989
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
990
// Return 5 commits (below limit)
991
vi.spyOn(mockRepo, 'log').mockResolvedValue(
992
Array.from({ length: 5 }, (_, i) => ({ hash: `commit${i}`, message: `msg${i}` })) as any
993
);
994
995
const repoTelemetry = new RepoInfoTelemetry(
996
'test-message-id',
997
telemetryService,
998
gitService,
999
gitDiffService,
1000
gitExtensionService,
1001
logService,
1002
fileSystemService,
1003
workspaceFileIndex,
1004
configurationService,
1005
copilotTokenStore
1006
);
1007
1008
await repoTelemetry.sendBeginTelemetryIfNeeded();
1009
1010
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1011
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1012
assert.strictEqual(call[1].result, 'success');
1013
assert.ok(call[1].diffsJSON);
1014
});
1015
1016
test('should skip with tooManyCommits result when log throws', async () => {
1017
setupInternalUser();
1018
mockGitServiceWithRepository();
1019
mockGitExtensionWithUpstream('abc123');
1020
1021
const mockApi = gitExtensionService.getExtensionApi();
1022
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
1023
vi.spyOn(mockRepo, 'log').mockRejectedValue(new Error('git log failed'));
1024
1025
const repoTelemetry = new RepoInfoTelemetry(
1026
'test-message-id',
1027
telemetryService,
1028
gitService,
1029
gitDiffService,
1030
gitExtensionService,
1031
logService,
1032
fileSystemService,
1033
workspaceFileIndex,
1034
configurationService,
1035
copilotTokenStore
1036
);
1037
1038
await repoTelemetry.sendBeginTelemetryIfNeeded();
1039
1040
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1041
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1042
assert.strictEqual(call[1].result, 'tooManyCommits');
1043
assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);
1044
});
1045
1046
// ========================================
1047
// Diff Too Big Tests
1048
// ========================================
1049
1050
test('should detect when there are too many changes', async () => {
1051
setupInternalUser();
1052
mockGitServiceWithRepository();
1053
mockGitExtensionWithUpstream('abc123');
1054
1055
// Create 101 changes (exceeds MAX_CHANGES of 100)
1056
const manyChanges = Array.from({ length: 101 }, (_, i) => ({
1057
uri: URI.file(`/test/repo/file${i}.ts`),
1058
originalUri: URI.file(`/test/repo/file${i}.ts`),
1059
renameUri: undefined,
1060
status: Status.MODIFIED
1061
}));
1062
1063
vi.spyOn(gitService, 'diffWith').mockResolvedValue(manyChanges as any);
1064
1065
const repoTelemetry = new RepoInfoTelemetry(
1066
'test-message-id',
1067
telemetryService,
1068
gitService,
1069
gitDiffService,
1070
gitExtensionService,
1071
logService,
1072
fileSystemService,
1073
workspaceFileIndex,
1074
configurationService,
1075
copilotTokenStore
1076
);
1077
1078
await repoTelemetry.sendBeginTelemetryIfNeeded();
1079
1080
// Assert: tooManyChanges result
1081
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1082
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1083
assert.strictEqual(call[1].result, 'tooManyChanges');
1084
assert.strictEqual(call[1].diffsJSON, undefined);
1085
assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');
1086
assert.strictEqual(call[1].headCommitHash, 'abc123');
1087
});
1088
1089
test('should detect when diff is too large', async () => {
1090
setupInternalUser();
1091
mockGitServiceWithRepository();
1092
mockGitExtensionWithUpstream('abc123');
1093
1094
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
1095
uri: URI.file('/test/repo/file.ts'),
1096
originalUri: URI.file('/test/repo/file.ts'),
1097
renameUri: undefined,
1098
status: Status.MODIFIED
1099
}] as any);
1100
1101
// Create a diff that exceeds 900KB when serialized to JSON
1102
const largeDiff = 'x'.repeat(901 * 1024);
1103
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{
1104
uri: URI.file('/test/repo/file.ts'),
1105
originalUri: URI.file('/test/repo/file.ts'),
1106
renameUri: undefined,
1107
status: Status.MODIFIED,
1108
diff: largeDiff
1109
}]);
1110
1111
const repoTelemetry = new RepoInfoTelemetry(
1112
'test-message-id',
1113
telemetryService,
1114
gitService,
1115
gitDiffService,
1116
gitExtensionService,
1117
logService,
1118
fileSystemService,
1119
workspaceFileIndex,
1120
configurationService,
1121
copilotTokenStore
1122
);
1123
1124
await repoTelemetry.sendBeginTelemetryIfNeeded();
1125
1126
// Assert: diffTooLarge result
1127
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1128
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1129
assert.strictEqual(call[1].result, 'diffTooLarge');
1130
assert.strictEqual(call[1].diffsJSON, undefined);
1131
assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');
1132
assert.strictEqual(call[1].headCommitHash, 'abc123');
1133
});
1134
1135
test('should send diff when within size limits', async () => {
1136
setupInternalUser();
1137
mockGitServiceWithRepository();
1138
mockGitExtensionWithUpstream('abc123');
1139
1140
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
1141
uri: URI.file('/test/repo/file.ts'),
1142
originalUri: URI.file('/test/repo/file.ts'),
1143
renameUri: undefined,
1144
status: Status.MODIFIED
1145
}] as any);
1146
1147
// Create a diff that is within limits
1148
const normalDiff = 'some normal diff content';
1149
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{
1150
uri: URI.file('/test/repo/file.ts'),
1151
originalUri: URI.file('/test/repo/file.ts'),
1152
renameUri: undefined,
1153
status: Status.MODIFIED,
1154
diff: normalDiff
1155
}]);
1156
1157
const repoTelemetry = new RepoInfoTelemetry(
1158
'test-message-id',
1159
telemetryService,
1160
gitService,
1161
gitDiffService,
1162
gitExtensionService,
1163
logService,
1164
fileSystemService,
1165
workspaceFileIndex,
1166
configurationService,
1167
copilotTokenStore
1168
);
1169
1170
await repoTelemetry.sendBeginTelemetryIfNeeded();
1171
1172
// Assert: success with diff
1173
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1174
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1175
assert.strictEqual(call[1].result, 'success');
1176
assert.ok(call[1].diffsJSON);
1177
1178
const diffs = JSON.parse(call[1].diffsJSON);
1179
assert.strictEqual(diffs.length, 1);
1180
assert.strictEqual(diffs[0].diff, normalDiff);
1181
});
1182
1183
test('should handle multiple files in diff', async () => {
1184
setupInternalUser();
1185
mockGitServiceWithRepository();
1186
mockGitExtensionWithUpstream('abc123');
1187
1188
vi.spyOn(gitService, 'diffWith').mockResolvedValue([
1189
{
1190
uri: URI.file('/test/repo/file1.ts'),
1191
originalUri: URI.file('/test/repo/file1.ts'),
1192
renameUri: undefined,
1193
status: Status.MODIFIED
1194
},
1195
{
1196
uri: URI.file('/test/repo/file2.ts'),
1197
originalUri: URI.file('/test/repo/file2.ts'),
1198
renameUri: undefined,
1199
status: Status.INDEX_ADDED
1200
},
1201
{
1202
uri: URI.file('/test/repo/file3.ts'),
1203
originalUri: URI.file('/test/repo/file3.ts'),
1204
renameUri: undefined,
1205
status: Status.DELETED
1206
}
1207
] as any);
1208
1209
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([
1210
{
1211
uri: URI.file('/test/repo/file1.ts'),
1212
originalUri: URI.file('/test/repo/file1.ts'),
1213
renameUri: undefined,
1214
status: Status.MODIFIED,
1215
diff: 'diff for file1'
1216
},
1217
{
1218
uri: URI.file('/test/repo/file2.ts'),
1219
originalUri: URI.file('/test/repo/file2.ts'),
1220
renameUri: undefined,
1221
status: Status.INDEX_ADDED,
1222
diff: 'diff for file2'
1223
},
1224
{
1225
uri: URI.file('/test/repo/file3.ts'),
1226
originalUri: URI.file('/test/repo/file3.ts'),
1227
renameUri: undefined,
1228
status: Status.DELETED,
1229
diff: 'diff for file3'
1230
}
1231
]);
1232
1233
const repoTelemetry = new RepoInfoTelemetry(
1234
'test-message-id',
1235
telemetryService,
1236
gitService,
1237
gitDiffService,
1238
gitExtensionService,
1239
logService,
1240
fileSystemService,
1241
workspaceFileIndex,
1242
configurationService,
1243
copilotTokenStore
1244
);
1245
1246
await repoTelemetry.sendBeginTelemetryIfNeeded();
1247
1248
// Assert: success with all diffs
1249
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1250
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1251
assert.strictEqual(call[1].result, 'success');
1252
1253
const diffs = JSON.parse(call[1].diffsJSON);
1254
assert.strictEqual(diffs.length, 3);
1255
assert.strictEqual(diffs[0].status, 'MODIFIED');
1256
assert.strictEqual(diffs[1].status, 'INDEX_ADDED');
1257
assert.strictEqual(diffs[2].status, 'DELETED');
1258
});
1259
1260
test('should handle renamed files in diff', async () => {
1261
setupInternalUser();
1262
mockGitServiceWithRepository();
1263
mockGitExtensionWithUpstream('abc123');
1264
1265
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
1266
uri: URI.file('/test/repo/newname.ts'),
1267
originalUri: URI.file('/test/repo/oldname.ts'),
1268
renameUri: URI.file('/test/repo/newname.ts'),
1269
status: Status.INDEX_RENAMED
1270
}] as any);
1271
1272
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{
1273
uri: URI.file('/test/repo/newname.ts'),
1274
originalUri: URI.file('/test/repo/oldname.ts'),
1275
renameUri: URI.file('/test/repo/newname.ts'),
1276
status: Status.INDEX_RENAMED,
1277
diff: 'diff content'
1278
}]);
1279
1280
const repoTelemetry = new RepoInfoTelemetry(
1281
'test-message-id',
1282
telemetryService,
1283
gitService,
1284
gitDiffService,
1285
gitExtensionService,
1286
logService,
1287
fileSystemService,
1288
workspaceFileIndex,
1289
configurationService,
1290
copilotTokenStore
1291
);
1292
1293
await repoTelemetry.sendBeginTelemetryIfNeeded();
1294
1295
// Assert: success with rename info
1296
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1297
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1298
assert.strictEqual(call[1].result, 'success');
1299
1300
const diffs = JSON.parse(call[1].diffsJSON);
1301
assert.strictEqual(diffs.length, 1);
1302
assert.strictEqual(diffs[0].status, 'INDEX_RENAMED');
1303
assert.ok(diffs[0].renameUri);
1304
});
1305
1306
test('should include untracked files from both workingTreeChanges and untrackedChanges', async () => {
1307
setupInternalUser();
1308
mockGitServiceWithRepository();
1309
1310
// Mock git extension with untracked files in both workingTreeChanges and untrackedChanges
1311
const mockRepo = {
1312
getMergeBase: vi.fn(),
1313
getBranchBase: vi.fn(),
1314
getCommit: vi.fn(),
1315
getConfig: vi.fn().mockResolvedValue(''),
1316
log: vi.fn().mockResolvedValue([]),
1317
state: {
1318
HEAD: {
1319
upstream: {
1320
commit: 'abc123',
1321
remote: 'origin',
1322
},
1323
},
1324
remotes: [{
1325
name: 'origin',
1326
fetchUrl: 'https://github.com/microsoft/vscode.git',
1327
pushUrl: 'https://github.com/microsoft/vscode.git',
1328
isReadOnly: false,
1329
}],
1330
workingTreeChanges: [{
1331
uri: URI.file('/test/repo/filea.txt'),
1332
originalUri: URI.file('/test/repo/filea.txt'),
1333
renameUri: undefined,
1334
status: Status.UNTRACKED
1335
}],
1336
untrackedChanges: [{
1337
uri: URI.file('/test/repo/fileb.txt'),
1338
originalUri: URI.file('/test/repo/fileb.txt'),
1339
renameUri: undefined,
1340
status: Status.UNTRACKED
1341
}],
1342
},
1343
};
1344
1345
mockRepo.getCommit.mockResolvedValue({
1346
hash: 'abc123',
1347
message: 'test commit',
1348
commitDate: new Date(),
1349
});
1350
1351
mockRepo.getMergeBase.mockImplementation(async (ref1: string, ref2: string) => {
1352
if (ref1 === 'HEAD' && ref2 === '@{upstream}') {
1353
return 'abc123';
1354
}
1355
return undefined;
1356
});
1357
1358
mockRepo.getBranchBase.mockResolvedValue(undefined);
1359
1360
const mockApi = {
1361
getRepository: () => mockRepo,
1362
};
1363
vi.spyOn(gitExtensionService, 'getExtensionApi').mockReturnValue(mockApi as any);
1364
1365
// Mock diffWith to return one modified file
1366
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
1367
uri: URI.file('/test/repo/modified.ts'),
1368
originalUri: URI.file('/test/repo/modified.ts'),
1369
renameUri: undefined,
1370
status: Status.MODIFIED
1371
}] as any);
1372
1373
// Mock diff service to return all three files
1374
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([
1375
{
1376
uri: URI.file('/test/repo/modified.ts'),
1377
originalUri: URI.file('/test/repo/modified.ts'),
1378
renameUri: undefined,
1379
status: Status.MODIFIED,
1380
diff: 'modified content'
1381
},
1382
{
1383
uri: URI.file('/test/repo/filea.txt'),
1384
originalUri: URI.file('/test/repo/filea.txt'),
1385
renameUri: undefined,
1386
status: Status.UNTRACKED,
1387
diff: 'new file a'
1388
},
1389
{
1390
uri: URI.file('/test/repo/fileb.txt'),
1391
originalUri: URI.file('/test/repo/fileb.txt'),
1392
renameUri: undefined,
1393
status: Status.UNTRACKED,
1394
diff: 'new file b'
1395
}
1396
]);
1397
1398
const repoTelemetry = new RepoInfoTelemetry(
1399
'test-message-id',
1400
telemetryService,
1401
gitService,
1402
gitDiffService,
1403
gitExtensionService,
1404
logService,
1405
fileSystemService,
1406
workspaceFileIndex,
1407
configurationService,
1408
copilotTokenStore
1409
);
1410
1411
await repoTelemetry.sendBeginTelemetryIfNeeded();
1412
1413
// Assert: success with all three files in telemetry
1414
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1415
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1416
assert.strictEqual(call[1].result, 'success');
1417
1418
const diffs = JSON.parse(call[1].diffsJSON);
1419
assert.strictEqual(diffs.length, 3, 'Should include 1 modified file + 2 untracked files');
1420
1421
// Verify all three files are present
1422
const uris = diffs.map((d: any) => d.uri);
1423
assert.ok(uris.includes('file:///test/repo/modified.ts'), 'Should include modified file');
1424
assert.ok(uris.includes('file:///test/repo/filea.txt'), 'Should include filea.txt from workingTreeChanges');
1425
assert.ok(uris.includes('file:///test/repo/fileb.txt'), 'Should include fileb.txt from untrackedChanges');
1426
1427
// Verify statuses
1428
const fileaEntry = diffs.find((d: any) => d.uri === 'file:///test/repo/filea.txt');
1429
const filebEntry = diffs.find((d: any) => d.uri === 'file:///test/repo/fileb.txt');
1430
assert.strictEqual(fileaEntry.status, 'UNTRACKED');
1431
assert.strictEqual(filebEntry.status, 'UNTRACKED');
1432
});
1433
1434
// ========================================
1435
// Measurements Tests
1436
// ========================================
1437
1438
test('should include workspaceFileCount in measurements', async () => {
1439
setupInternalUser();
1440
mockGitServiceWithRepository();
1441
mockGitExtensionWithUpstream('abc123');
1442
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1443
1444
// Set a specific file count
1445
(workspaceFileIndex as any).fileCount = 250;
1446
1447
const repoTelemetry = new RepoInfoTelemetry(
1448
'test-message-id',
1449
telemetryService,
1450
gitService,
1451
gitDiffService,
1452
gitExtensionService,
1453
logService,
1454
fileSystemService,
1455
workspaceFileIndex,
1456
configurationService,
1457
copilotTokenStore
1458
);
1459
1460
await repoTelemetry.sendBeginTelemetryIfNeeded();
1461
1462
// Assert: measurements contain workspaceFileCount
1463
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1464
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1465
assert.ok(call[2], 'measurements parameter should exist');
1466
assert.strictEqual(call[2].workspaceFileCount, 250);
1467
});
1468
1469
test('should include changedFileCount in measurements', async () => {
1470
setupInternalUser();
1471
mockGitServiceWithRepository();
1472
mockGitExtensionWithUpstream('abc123');
1473
1474
// Mock 5 changes
1475
const changes = Array.from({ length: 5 }, (_, i) => ({
1476
uri: URI.file(`/test/repo/file${i}.ts`),
1477
originalUri: URI.file(`/test/repo/file${i}.ts`),
1478
renameUri: undefined,
1479
status: Status.MODIFIED
1480
}));
1481
1482
vi.spyOn(gitService, 'diffWith').mockResolvedValue(changes as any);
1483
1484
vi.spyOn(gitDiffService, 'getChangeDiffs').mockResolvedValue(
1485
changes.map((c, i) => ({
1486
uri: URI.file(`/test/repo/file${i}.ts`),
1487
originalUri: URI.file(`/test/repo/file${i}.ts`),
1488
renameUri: undefined,
1489
status: Status.MODIFIED,
1490
diff: `diff for file${i}`
1491
}))
1492
);
1493
1494
const repoTelemetry = new RepoInfoTelemetry(
1495
'test-message-id',
1496
telemetryService,
1497
gitService,
1498
gitDiffService,
1499
gitExtensionService,
1500
logService,
1501
fileSystemService,
1502
workspaceFileIndex,
1503
configurationService,
1504
copilotTokenStore
1505
);
1506
1507
await repoTelemetry.sendBeginTelemetryIfNeeded();
1508
1509
// Assert: measurements contain changedFileCount
1510
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1511
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1512
assert.ok(call[2], 'measurements parameter should exist');
1513
assert.strictEqual(call[2].changedFileCount, 5);
1514
});
1515
1516
test('should set changedFileCount to 0 when no changes', async () => {
1517
setupInternalUser();
1518
mockGitServiceWithRepository();
1519
mockGitExtensionWithUpstream('abc123');
1520
1521
// Mock: no changes from upstream
1522
vi.spyOn(gitService, 'diffWith').mockResolvedValue([]);
1523
1524
const repoTelemetry = new RepoInfoTelemetry(
1525
'test-message-id',
1526
telemetryService,
1527
gitService,
1528
gitDiffService,
1529
gitExtensionService,
1530
logService,
1531
fileSystemService,
1532
workspaceFileIndex,
1533
configurationService,
1534
copilotTokenStore
1535
);
1536
1537
await repoTelemetry.sendBeginTelemetryIfNeeded();
1538
1539
// Assert: changedFileCount is 0
1540
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1541
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1542
assert.ok(call[2], 'measurements parameter should exist');
1543
assert.strictEqual(call[2].changedFileCount, 0);
1544
});
1545
1546
test('should include measurements in both begin and end telemetry', async () => {
1547
setupInternalUser();
1548
mockGitServiceWithRepository();
1549
mockGitExtensionWithUpstream('abc123');
1550
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1551
1552
(workspaceFileIndex as any).fileCount = 150;
1553
1554
const repoTelemetry = new RepoInfoTelemetry(
1555
'test-message-id',
1556
telemetryService,
1557
gitService,
1558
gitDiffService,
1559
gitExtensionService,
1560
logService,
1561
fileSystemService,
1562
workspaceFileIndex,
1563
configurationService,
1564
copilotTokenStore
1565
);
1566
1567
await repoTelemetry.sendBeginTelemetryIfNeeded();
1568
await repoTelemetry.sendEndTelemetry();
1569
1570
// Assert: both begin and end have measurements
1571
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);
1572
1573
const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1574
assert.ok(beginCall[2], 'begin measurements should exist');
1575
assert.strictEqual(beginCall[2].workspaceFileCount, 150);
1576
assert.strictEqual(beginCall[2].changedFileCount, 1);
1577
1578
const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];
1579
assert.ok(endCall[2], 'end measurements should exist');
1580
assert.strictEqual(endCall[2].workspaceFileCount, 150);
1581
assert.strictEqual(endCall[2].changedFileCount, 1);
1582
});
1583
1584
test('should include measurements even when diff is too large', async () => {
1585
setupInternalUser();
1586
mockGitServiceWithRepository();
1587
mockGitExtensionWithUpstream('abc123');
1588
1589
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
1590
uri: URI.file('/test/repo/file.ts'),
1591
originalUri: URI.file('/test/repo/file.ts'),
1592
renameUri: undefined,
1593
status: Status.MODIFIED
1594
}] as any);
1595
1596
// Create a diff that exceeds 900KB when serialized to JSON
1597
const largeDiff = 'x'.repeat(901 * 1024);
1598
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{
1599
uri: URI.file('/test/repo/file.ts'),
1600
originalUri: URI.file('/test/repo/file.ts'),
1601
renameUri: undefined,
1602
status: Status.MODIFIED,
1603
diff: largeDiff
1604
}]);
1605
1606
(workspaceFileIndex as any).fileCount = 200;
1607
1608
const repoTelemetry = new RepoInfoTelemetry(
1609
'test-message-id',
1610
telemetryService,
1611
gitService,
1612
gitDiffService,
1613
gitExtensionService,
1614
logService,
1615
fileSystemService,
1616
workspaceFileIndex,
1617
configurationService,
1618
copilotTokenStore
1619
);
1620
1621
await repoTelemetry.sendBeginTelemetryIfNeeded();
1622
1623
// Assert: diffTooLarge result but measurements still present
1624
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1625
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1626
assert.strictEqual(call[1].result, 'diffTooLarge');
1627
assert.ok(call[2], 'measurements should still be present');
1628
assert.strictEqual(call[2].workspaceFileCount, 200);
1629
assert.strictEqual(call[2].changedFileCount, 1);
1630
});
1631
1632
test('should include measurements when there are too many changes', async () => {
1633
setupInternalUser();
1634
mockGitServiceWithRepository();
1635
mockGitExtensionWithUpstream('abc123');
1636
1637
// Create 101 changes (exceeds MAX_CHANGES of 100)
1638
const manyChanges = Array.from({ length: 101 }, (_, i) => ({
1639
uri: URI.file(`/test/repo/file${i}.ts`),
1640
originalUri: URI.file(`/test/repo/file${i}.ts`),
1641
renameUri: undefined,
1642
status: Status.MODIFIED
1643
}));
1644
1645
vi.spyOn(gitService, 'diffWith').mockResolvedValue(manyChanges as any);
1646
1647
(workspaceFileIndex as any).fileCount = 300;
1648
1649
const repoTelemetry = new RepoInfoTelemetry(
1650
'test-message-id',
1651
telemetryService,
1652
gitService,
1653
gitDiffService,
1654
gitExtensionService,
1655
logService,
1656
fileSystemService,
1657
workspaceFileIndex,
1658
configurationService,
1659
copilotTokenStore
1660
);
1661
1662
await repoTelemetry.sendBeginTelemetryIfNeeded();
1663
1664
// Assert: tooManyChanges result but measurements still present
1665
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1666
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1667
assert.strictEqual(call[1].result, 'tooManyChanges');
1668
assert.ok(call[2], 'measurements should still be present');
1669
assert.strictEqual(call[2].workspaceFileCount, 300);
1670
assert.strictEqual(call[2].changedFileCount, 101);
1671
});
1672
1673
test('should include diffSizeBytes in measurements when diffs are present', async () => {
1674
setupInternalUser();
1675
mockGitServiceWithRepository();
1676
mockGitExtensionWithUpstream('abc123');
1677
1678
const testDiff = 'diff --git a/file.ts b/file.ts\n--- a/file.ts\n+++ b/file.ts\n@@ -1,1 +1,1 @@\n-old\n+new';
1679
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: testDiff }]);
1680
1681
const repoTelemetry = new RepoInfoTelemetry(
1682
'test-message-id',
1683
telemetryService,
1684
gitService,
1685
gitDiffService,
1686
gitExtensionService,
1687
logService,
1688
fileSystemService,
1689
workspaceFileIndex,
1690
configurationService,
1691
copilotTokenStore
1692
);
1693
1694
await repoTelemetry.sendBeginTelemetryIfNeeded();
1695
1696
// Assert: diffSizeBytes measurement is set
1697
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1698
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1699
assert.strictEqual(call[1].result, 'success');
1700
assert.ok(call[2], 'measurements parameter should be present');
1701
assert.strictEqual(typeof call[2].diffSizeBytes, 'number');
1702
assert.ok(call[2].diffSizeBytes > 0, 'diffSizeBytes should be greater than 0');
1703
1704
// Calculate expected size from the mock data
1705
const expectedDiffsJSON = JSON.stringify([{
1706
uri: 'file:///test/repo/file.ts',
1707
originalUri: 'file:///test/repo/file.ts',
1708
renameUri: undefined,
1709
status: 'MODIFIED',
1710
diff: testDiff
1711
}]);
1712
const expectedSize = Buffer.byteLength(expectedDiffsJSON, 'utf8');
1713
assert.strictEqual(call[2].diffSizeBytes, expectedSize);
1714
});
1715
1716
// ========================================
1717
// Error Handling Tests
1718
// ========================================
1719
1720
test('should handle errors during git diff gracefully', async () => {
1721
setupInternalUser();
1722
mockGitServiceWithRepository();
1723
mockGitExtensionWithUpstream('abc123');
1724
1725
// Mock git diff to throw error
1726
vi.spyOn(gitService, 'diffWith').mockRejectedValue(new Error('Git error'));
1727
1728
const repoTelemetry = new RepoInfoTelemetry(
1729
'test-message-id',
1730
telemetryService,
1731
gitService,
1732
gitDiffService,
1733
gitExtensionService,
1734
logService,
1735
fileSystemService,
1736
workspaceFileIndex,
1737
configurationService,
1738
copilotTokenStore
1739
);
1740
1741
// Should not throw
1742
await repoTelemetry.sendBeginTelemetryIfNeeded();
1743
1744
// Assert: no telemetry sent due to error
1745
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);
1746
});
1747
1748
test('should handle errors during diff processing gracefully', async () => {
1749
setupInternalUser();
1750
mockGitServiceWithRepository();
1751
mockGitExtensionWithUpstream('abc123');
1752
1753
vi.spyOn(gitService, 'diffWith').mockResolvedValue([{
1754
uri: URI.file('/test/repo/file.ts'),
1755
originalUri: URI.file('/test/repo/file.ts'),
1756
renameUri: undefined,
1757
status: Status.MODIFIED
1758
}] as any);
1759
1760
// Mock diff service to throw error
1761
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockRejectedValue(new Error('Diff processing error'));
1762
1763
const repoTelemetry = new RepoInfoTelemetry(
1764
'test-message-id',
1765
telemetryService,
1766
gitService,
1767
gitDiffService,
1768
gitExtensionService,
1769
logService,
1770
fileSystemService,
1771
workspaceFileIndex,
1772
configurationService,
1773
copilotTokenStore
1774
);
1775
1776
// Should not throw
1777
await repoTelemetry.sendBeginTelemetryIfNeeded();
1778
1779
// Assert: no telemetry sent due to error
1780
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);
1781
});
1782
1783
// ========================================
1784
// Disable Setting and Merge Base Age Tests
1785
// ========================================
1786
1787
test('should skip telemetry when disableRepoInfoTelemetry setting is enabled', async () => {
1788
setupInternalUser();
1789
mockGitServiceWithRepository();
1790
mockGitExtensionWithUpstream('abc123');
1791
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1792
1793
// Enable the disable setting
1794
(configurationService as InMemoryConfigurationService).setConfig(
1795
ConfigKey.TeamInternal.DisableRepoInfoTelemetry, true
1796
);
1797
1798
const repoTelemetry = new RepoInfoTelemetry(
1799
'test-message-id',
1800
telemetryService,
1801
gitService,
1802
gitDiffService,
1803
gitExtensionService,
1804
logService,
1805
fileSystemService,
1806
workspaceFileIndex,
1807
configurationService,
1808
copilotTokenStore
1809
);
1810
1811
await repoTelemetry.sendBeginTelemetryIfNeeded();
1812
1813
// Assert: no telemetry sent
1814
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);
1815
});
1816
1817
test('should return mergeBaseTooOld when upstream commit is older than 30 days', async () => {
1818
setupInternalUser();
1819
mockGitServiceWithRepository();
1820
mockGitExtensionWithUpstream('old-commit-abc');
1821
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1822
1823
// Override getCommit to return a commit older than 30 days
1824
const mockApi = gitExtensionService.getExtensionApi();
1825
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
1826
(mockRepo as any).getCommit.mockResolvedValue({
1827
hash: 'old-commit-abc',
1828
message: 'old commit',
1829
commitDate: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000), // 45 days ago
1830
});
1831
1832
const repoTelemetry = new RepoInfoTelemetry(
1833
'test-message-id',
1834
telemetryService,
1835
gitService,
1836
gitDiffService,
1837
gitExtensionService,
1838
logService,
1839
fileSystemService,
1840
workspaceFileIndex,
1841
configurationService,
1842
copilotTokenStore
1843
);
1844
1845
await repoTelemetry.sendBeginTelemetryIfNeeded();
1846
1847
// Assert: telemetry sent with mergeBaseTooOld result
1848
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1849
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1850
assert.strictEqual(call[1].result, 'mergeBaseTooOld');
1851
assert.strictEqual(call[1].diffsJSON, undefined);
1852
});
1853
1854
test('should proceed normally when upstream commit is within 30 days', async () => {
1855
setupInternalUser();
1856
mockGitServiceWithRepository();
1857
mockGitExtensionWithUpstream('recent-commit');
1858
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1859
1860
// getCommit already returns a recent commit by default in the mock
1861
1862
const repoTelemetry = new RepoInfoTelemetry(
1863
'test-message-id',
1864
telemetryService,
1865
gitService,
1866
gitDiffService,
1867
gitExtensionService,
1868
logService,
1869
fileSystemService,
1870
workspaceFileIndex,
1871
configurationService,
1872
copilotTokenStore
1873
);
1874
1875
await repoTelemetry.sendBeginTelemetryIfNeeded();
1876
1877
// Assert: telemetry sent with success result (not mergeBaseTooOld)
1878
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1879
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1880
assert.strictEqual(call[1].result, 'success');
1881
});
1882
1883
test('should return mergeBaseTooOld when getCommit fails', async () => {
1884
setupInternalUser();
1885
mockGitServiceWithRepository();
1886
mockGitExtensionWithUpstream('abc123');
1887
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1888
1889
// Override getCommit to throw
1890
const mockApi = gitExtensionService.getExtensionApi();
1891
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
1892
(mockRepo as any).getCommit.mockRejectedValue(new Error('Failed to get commit'));
1893
1894
const repoTelemetry = new RepoInfoTelemetry(
1895
'test-message-id',
1896
telemetryService,
1897
gitService,
1898
gitDiffService,
1899
gitExtensionService,
1900
logService,
1901
fileSystemService,
1902
workspaceFileIndex,
1903
configurationService,
1904
copilotTokenStore
1905
);
1906
1907
await repoTelemetry.sendBeginTelemetryIfNeeded();
1908
1909
// Assert: telemetry sent with mergeBaseTooOld result
1910
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1911
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1912
assert.strictEqual(call[1].result, 'mergeBaseTooOld');
1913
});
1914
1915
test('should return mergeBaseTooOld when commit date is undefined', async () => {
1916
setupInternalUser();
1917
mockGitServiceWithRepository();
1918
mockGitExtensionWithUpstream('abc123');
1919
mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);
1920
1921
// Override getCommit to return a commit without a date
1922
const mockApi = gitExtensionService.getExtensionApi();
1923
const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;
1924
(mockRepo as any).getCommit.mockResolvedValue({
1925
hash: 'abc123',
1926
message: 'commit without date',
1927
commitDate: undefined,
1928
});
1929
1930
const repoTelemetry = new RepoInfoTelemetry(
1931
'test-message-id',
1932
telemetryService,
1933
gitService,
1934
gitDiffService,
1935
gitExtensionService,
1936
logService,
1937
fileSystemService,
1938
workspaceFileIndex,
1939
configurationService,
1940
copilotTokenStore
1941
);
1942
1943
await repoTelemetry.sendBeginTelemetryIfNeeded();
1944
1945
// Assert: telemetry sent with mergeBaseTooOld result
1946
assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);
1947
const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];
1948
assert.strictEqual(call[1].result, 'mergeBaseTooOld');
1949
});
1950
1951
// ========================================
1952
// Helper Functions
1953
// ========================================
1954
1955
function setupInternalUser() {
1956
const internalToken = new CopilotToken(createTestExtendedTokenInfo({
1957
token: 'tid=test;rt=1',
1958
sku: 'free_limited_copilot',
1959
expires_at: 9999999999,
1960
refresh_in: 180000,
1961
organization_list: ['4535c7beffc844b46bb1ed4aa04d759a'], // GitHub org for internal users
1962
isVscodeTeamMember: true,
1963
username: 'testUser',
1964
copilot_plan: 'unknown',
1965
}));
1966
copilotTokenStore.copilotToken = internalToken;
1967
}
1968
1969
function mockGitServiceWithRepository() {
1970
vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({
1971
rootUri: URI.file('/test/repo'),
1972
changes: {
1973
mergeChanges: [],
1974
indexChanges: [],
1975
workingTree: [{
1976
uri: URI.file('/test/repo/file.ts'),
1977
originalUri: URI.file('/test/repo/file.ts'),
1978
renameUri: undefined,
1979
status: Status.MODIFIED
1980
}],
1981
untrackedChanges: []
1982
},
1983
remotes: ['origin'],
1984
remoteFetchUrls: ['https://github.com/microsoft/vscode.git'],
1985
upstreamRemote: 'origin',
1986
headBranchName: 'main',
1987
headCommitHash: 'abc123',
1988
upstreamBranchName: 'origin/main',
1989
isRebasing: false,
1990
} as any);
1991
}
1992
1993
function mockGitExtensionWithUpstream(upstreamCommit: string | undefined, remoteUrl: string = 'https://github.com/microsoft/vscode.git') {
1994
const mockRepo = {
1995
getMergeBase: vi.fn(),
1996
getBranchBase: vi.fn(),
1997
getCommit: vi.fn(),
1998
getConfig: vi.fn().mockResolvedValue(''),
1999
log: vi.fn().mockResolvedValue([]),
2000
state: {
2001
HEAD: {
2002
upstream: upstreamCommit ? {
2003
commit: upstreamCommit,
2004
remote: 'origin',
2005
} : undefined,
2006
},
2007
remotes: [{
2008
name: 'origin',
2009
fetchUrl: remoteUrl,
2010
pushUrl: remoteUrl,
2011
isReadOnly: false,
2012
}],
2013
workingTreeChanges: [],
2014
untrackedChanges: [],
2015
},
2016
};
2017
2018
// Set up getMergeBase to return upstreamCommit when called with 'HEAD' and '@upstream'
2019
mockRepo.getMergeBase.mockImplementation(async (ref1: string, ref2: string) => {
2020
if (ref1 === 'HEAD' && ref2 === '@{upstream}') {
2021
return upstreamCommit;
2022
}
2023
return undefined;
2024
});
2025
2026
// Set up getBranchBase to return undefined by default
2027
mockRepo.getBranchBase.mockResolvedValue(undefined);
2028
2029
// Set up getCommit to return a recent commit by default
2030
mockRepo.getCommit.mockResolvedValue({
2031
hash: upstreamCommit ?? 'abc123',
2032
message: 'test commit',
2033
commitDate: new Date(),
2034
});
2035
2036
const mockApi = {
2037
getRepository: () => mockRepo,
2038
};
2039
vi.spyOn(gitExtensionService, 'getExtensionApi').mockReturnValue(mockApi as any);
2040
}
2041
2042
function mockGitDiffService(diffs: any[]) {
2043
// Mock diffWith to return Change objects
2044
const changes = diffs.map(d => ({
2045
uri: URI.file(d.uri || '/test/repo/file.ts'),
2046
originalUri: URI.file(d.originalUri || d.uri || '/test/repo/file.ts'),
2047
renameUri: d.renameUri ? URI.file(d.renameUri) : undefined,
2048
status: d.status || Status.MODIFIED
2049
}));
2050
2051
vi.spyOn(gitService, 'diffWith').mockResolvedValue(
2052
diffs.length > 0 ? changes as any : []
2053
);
2054
2055
// Mock getWorkingTreeDiffsFromRef to return Diff objects (Change + diff property)
2056
vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue(
2057
diffs.map(d => ({
2058
uri: URI.file(d.uri || '/test/repo/file.ts'),
2059
originalUri: URI.file(d.originalUri || d.uri || '/test/repo/file.ts'),
2060
renameUri: d.renameUri ? URI.file(d.renameUri) : undefined,
2061
status: d.status || Status.MODIFIED,
2062
diff: d.diff || 'test diff'
2063
}))
2064
);
2065
}
2066
});
2067
2068
// ========================================
2069
// Mock File System Watcher
2070
// ========================================
2071
2072
class MockFileSystemWatcher implements FileSystemWatcher {
2073
private _createHandlers: ((e: Uri) => any)[] = [];
2074
private _changeHandlers: ((e: Uri) => any)[] = [];
2075
private _deleteHandlers: ((e: Uri) => any)[] = [];
2076
public isDisposed = false;
2077
public ignoreCreateEvents = false;
2078
public ignoreChangeEvents = false;
2079
public ignoreDeleteEvents = false;
2080
2081
get onDidCreate(): Event<Uri> {
2082
return (listener) => {
2083
this._createHandlers.push(listener);
2084
return {
2085
dispose: () => {
2086
const index = this._createHandlers.indexOf(listener);
2087
if (index > -1) {
2088
this._createHandlers.splice(index, 1);
2089
}
2090
}
2091
};
2092
};
2093
}
2094
2095
get onDidChange(): Event<Uri> {
2096
return (listener) => {
2097
this._changeHandlers.push(listener);
2098
return {
2099
dispose: () => {
2100
const index = this._changeHandlers.indexOf(listener);
2101
if (index > -1) {
2102
this._changeHandlers.splice(index, 1);
2103
}
2104
}
2105
};
2106
};
2107
}
2108
2109
get onDidDelete(): Event<Uri> {
2110
return (listener) => {
2111
this._deleteHandlers.push(listener);
2112
return {
2113
dispose: () => {
2114
const index = this._deleteHandlers.indexOf(listener);
2115
if (index > -1) {
2116
this._deleteHandlers.splice(index, 1);
2117
}
2118
}
2119
};
2120
};
2121
}
2122
2123
triggerCreate(uri: Uri): void {
2124
this._createHandlers.forEach(h => h(uri));
2125
}
2126
2127
triggerChange(uri: Uri): void {
2128
this._changeHandlers.forEach(h => h(uri));
2129
}
2130
2131
triggerDelete(uri: Uri): void {
2132
this._deleteHandlers.forEach(h => h(uri));
2133
}
2134
2135
dispose(): void {
2136
this.isDisposed = true;
2137
this._createHandlers = [];
2138
this._changeHandlers = [];
2139
this._deleteHandlers = [];
2140
}
2141
}
2142
2143