Path: blob/main/extensions/copilot/src/extension/prompt/node/test/repoInfoTelemetry.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { beforeEach, suite, test, vi } from 'vitest';7import type { FileSystemWatcher, Uri } from 'vscode';8import { CopilotToken, createTestExtendedTokenInfo } from '../../../../platform/authentication/common/copilotToken';9import { ICopilotTokenStore } from '../../../../platform/authentication/common/copilotTokenStore';10import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';11import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';12import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';13import { IGitDiffService } from '../../../../platform/git/common/gitDiffService';14import { IGitExtensionService } from '../../../../platform/git/common/gitExtensionService';15import { IGitService } from '../../../../platform/git/common/gitService';16import { NullGitDiffService } from '../../../../platform/git/common/nullGitDiffService';17import { NullGitExtensionService } from '../../../../platform/git/common/nullGitExtensionService';18import { ILogService } from '../../../../platform/log/common/logService';19import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';20import { createPlatformServices } from '../../../../platform/test/node/services';21import { NullWorkspaceFileIndex } from '../../../../platform/workspaceChunkSearch/node/nullWorkspaceFileIndex';22import { IWorkspaceFileIndex } from '../../../../platform/workspaceChunkSearch/node/workspaceFileIndex';23import { Event } from '../../../../util/vs/base/common/event';24import { observableValue } from '../../../../util/vs/base/common/observableInternal/observables/observableValue';25import { URI } from '../../../../util/vs/base/common/uri';26import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';27import { RepoInfoTelemetry } from '../repoInfoTelemetry';2829// Import Status enum - use const enum values directly since vitest doesn't handle .d.ts well30const Status = {31INDEX_MODIFIED: 0,32INDEX_ADDED: 1,33INDEX_DELETED: 2,34INDEX_RENAMED: 3,35INDEX_COPIED: 4,36MODIFIED: 5,37DELETED: 6,38UNTRACKED: 7,39IGNORED: 8,40INTENT_TO_ADD: 9,41INTENT_TO_RENAME: 10,42TYPE_CHANGED: 11,43ADDED_BY_US: 12,44ADDED_BY_THEM: 13,45DELETED_BY_US: 14,46DELETED_BY_THEM: 15,47BOTH_ADDED: 16,48BOTH_DELETED: 17,49BOTH_MODIFIED: 1850} as const;5152suite('RepoInfoTelemetry', () => {53let accessor: ReturnType<ReturnType<typeof createPlatformServices>['createTestingAccessor']>;54let telemetryService: ITelemetryService;55let gitService: IGitService;56let gitDiffService: IGitDiffService;57let gitExtensionService: IGitExtensionService;58let copilotTokenStore: ICopilotTokenStore;59let logService: ILogService;60let fileSystemService: IFileSystemService;61let workspaceFileIndex: IWorkspaceFileIndex;62let configurationService: IConfigurationService;63let mockWatcher: MockFileSystemWatcher;6465beforeEach(() => {66const services = createPlatformServices();67// Register extension-level services not in platform services by default68services.define(IGitDiffService, new SyncDescriptor(NullGitDiffService));69services.define(IGitExtensionService, new NullGitExtensionService());70services.define(IWorkspaceFileIndex, new SyncDescriptor(NullWorkspaceFileIndex));7172// Override IGitService with a proper mock that has an observable activeRepository73const mockGitService: IGitService = {74_serviceBrand: undefined,75activeRepository: observableValue('test-git-activeRepo', undefined),76onDidOpenRepository: Event.None,77onDidCloseRepository: Event.None,78onDidFinishInitialization: Event.None,79repositories: [],80isInitialized: true,81initRepository: vi.fn(),82openRepository: vi.fn(),83getRepository: vi.fn(),84getRepository2: vi.fn(),85getRecentRepositories: vi.fn(),86getRepositoryFetchUrls: vi.fn(),87generateRandomBranchName: vi.fn(),88initialize: vi.fn(),89diffBetweenWithStats: vi.fn(),90diffBetweenPatch: vi.fn(),91diffWith: vi.fn(),92diffIndexWithHEADShortStats: vi.fn(),93getMergeBase: vi.fn(),94restore: vi.fn(),95add: vi.fn(),96createWorktree: vi.fn(),97deleteWorktree: vi.fn(),98migrateChanges: vi.fn(),99applyPatch: vi.fn(),100commit: vi.fn(),101getRefs: vi.fn(),102getBranch: vi.fn(),103getBranchBase: vi.fn(),104isBranchProtected: vi.fn(),105exec: vi.fn(),106dispose: vi.fn()107};108services.define(IGitService, mockGitService);109110accessor = services.createTestingAccessor();111112telemetryService = accessor.get(ITelemetryService);113gitService = accessor.get(IGitService);114gitDiffService = accessor.get(IGitDiffService);115gitExtensionService = accessor.get(IGitExtensionService);116copilotTokenStore = accessor.get(ICopilotTokenStore);117logService = accessor.get(ILogService);118fileSystemService = accessor.get(IFileSystemService);119workspaceFileIndex = accessor.get(IWorkspaceFileIndex);120configurationService = accessor.get(IConfigurationService);121122// Create a new mock watcher for each test123mockWatcher = new MockFileSystemWatcher();124125// Mock the file system service to return our mock watcher126vi.spyOn(fileSystemService, 'createFileSystemWatcher').mockReturnValue(mockWatcher as any);127128// Properly mock the telemetry methods129(telemetryService as any).sendMSFTTelemetryEvent = vi.fn();130(telemetryService as any).sendInternalMSFTTelemetryEvent = vi.fn();131});132133// ========================================134// Basic Telemetry Flow Tests135// ========================================136137test('should not send any telemetry for non-internal users', async () => {138// Setup: non-internal user139const nonInternalToken = new CopilotToken(createTestExtendedTokenInfo({140token: 'test-token',141sku: 'free_limited_copilot',142expires_at: 9999999999,143refresh_in: 180000,144organization_list: [],145isVscodeTeamMember: false,146username: 'testUser',147copilot_plan: 'unknown',148}));149copilotTokenStore.copilotToken = nonInternalToken;150151// Setup: mock git service to have a repository152mockGitServiceWithRepository();153mockGitExtensionWithUpstream('abc123');154mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);155156const repoTelemetry = new RepoInfoTelemetry(157'test-message-id',158telemetryService,159gitService,160gitDiffService,161gitExtensionService,162logService,163fileSystemService,164workspaceFileIndex,165configurationService,166copilotTokenStore167);168169await repoTelemetry.sendBeginTelemetryIfNeeded();170await repoTelemetry.sendEndTelemetry();171172// Assert: no telemetry sent for non-internal users173assert.strictEqual((telemetryService.sendMSFTTelemetryEvent as any).mock.calls.length, 0, 'sendMSFTTelemetryEvent should not be called for non-internal users');174assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0, 'sendInternalMSFTTelemetryEvent should not be called for non-internal users');175});176177test('should send telemetry for internal users', async () => {178// Setup: internal user179setupInternalUser();180mockGitServiceWithRepository();181mockGitExtensionWithUpstream('abc123');182mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);183184const repoTelemetry = new RepoInfoTelemetry(185'test-message-id',186telemetryService,187gitService,188gitDiffService,189gitExtensionService,190logService,191fileSystemService,192workspaceFileIndex,193configurationService,194copilotTokenStore195);196197await repoTelemetry.sendBeginTelemetryIfNeeded();198199// Assert: begin telemetry sent200assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);201const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];202assert.strictEqual(call[0], 'request.repoInfo');203assert.strictEqual(call[1].location, 'begin');204assert.strictEqual(call[1].telemetryMessageId, 'test-message-id');205// Check measurements parameter exists206assert.ok(call[2], 'measurements parameter should be present');207assert.strictEqual(typeof call[2].workspaceFileCount, 'number');208});209210test('should send begin telemetry only once', async () => {211setupInternalUser();212mockGitServiceWithRepository();213mockGitExtensionWithUpstream('abc123');214mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);215216const repoTelemetry = new RepoInfoTelemetry(217'test-message-id',218telemetryService,219gitService,220gitDiffService,221gitExtensionService,222logService,223fileSystemService,224workspaceFileIndex,225configurationService,226copilotTokenStore227);228229await repoTelemetry.sendBeginTelemetryIfNeeded();230await repoTelemetry.sendBeginTelemetryIfNeeded();231await repoTelemetry.sendBeginTelemetryIfNeeded();232233// Assert: only one begin telemetry sent234assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);235});236237test('should send end telemetry after begin', async () => {238setupInternalUser();239mockGitServiceWithRepository();240mockGitExtensionWithUpstream('abc123');241mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);242243const repoTelemetry = new RepoInfoTelemetry(244'test-message-id',245telemetryService,246gitService,247gitDiffService,248gitExtensionService,249logService,250fileSystemService,251workspaceFileIndex,252configurationService,253copilotTokenStore254);255256await repoTelemetry.sendBeginTelemetryIfNeeded();257await repoTelemetry.sendEndTelemetry();258259// Assert: both begin and end telemetry sent260assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);261const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];262const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];263264assert.strictEqual(beginCall[1].location, 'begin');265assert.strictEqual(endCall[1].location, 'end');266assert.strictEqual(beginCall[1].telemetryMessageId, endCall[1].telemetryMessageId);267});268269test('should send end telemetry when begin has success result', async () => {270setupInternalUser();271mockGitServiceWithRepository();272mockGitExtensionWithUpstream('abc123');273mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);274275const repoTelemetry = new RepoInfoTelemetry(276'test-message-id',277telemetryService,278gitService,279gitDiffService,280gitExtensionService,281logService,282fileSystemService,283workspaceFileIndex,284configurationService,285copilotTokenStore286);287288await repoTelemetry.sendBeginTelemetryIfNeeded();289await repoTelemetry.sendEndTelemetry();290291// Assert: both begin and end telemetry sent292assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);293const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];294const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];295assert.strictEqual(beginCall[1].location, 'begin');296assert.strictEqual(beginCall[1].result, 'success');297assert.strictEqual(endCall[1].location, 'end');298assert.strictEqual(endCall[1].result, 'success');299});300301test('should send end telemetry when begin has noChanges result', async () => {302setupInternalUser();303mockGitServiceWithRepository();304mockGitExtensionWithUpstream('abc123');305306// Mock: no changes from upstream307vi.spyOn(gitService, 'diffWith').mockResolvedValue([]);308309const repoTelemetry = new RepoInfoTelemetry(310'test-message-id',311telemetryService,312gitService,313gitDiffService,314gitExtensionService,315logService,316fileSystemService,317workspaceFileIndex,318configurationService,319copilotTokenStore320);321322await repoTelemetry.sendBeginTelemetryIfNeeded();323await repoTelemetry.sendEndTelemetry();324325// Assert: both begin and end telemetry sent326assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);327const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];328const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];329assert.strictEqual(beginCall[1].location, 'begin');330assert.strictEqual(beginCall[1].result, 'noChanges');331assert.strictEqual(endCall[1].location, 'end');332assert.strictEqual(endCall[1].result, 'noChanges');333});334335test('should skip end telemetry when begin has failure result', async () => {336setupInternalUser();337mockGitServiceWithRepository();338mockGitExtensionWithUpstream('abc123');339340// Mock: too many changes (failure result)341const manyChanges = Array.from({ length: 101 }, (_, i) => ({342uri: URI.file(`/test/repo/file${i}.ts`),343originalUri: URI.file(`/test/repo/file${i}.ts`),344renameUri: undefined,345status: Status.MODIFIED346}));347vi.spyOn(gitService, 'diffWith').mockResolvedValue(manyChanges as any);348349const repoTelemetry = new RepoInfoTelemetry(350'test-message-id',351telemetryService,352gitService,353gitDiffService,354gitExtensionService,355logService,356fileSystemService,357workspaceFileIndex,358configurationService,359copilotTokenStore360);361362await repoTelemetry.sendBeginTelemetryIfNeeded();363await repoTelemetry.sendEndTelemetry();364365// Assert: only begin telemetry sent, end was skipped366assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);367const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];368assert.strictEqual(beginCall[1].location, 'begin');369assert.strictEqual(beginCall[1].result, 'tooManyChanges');370});371372// ========================================373// Git Repository Detection Tests374// ========================================375376test('should not send telemetry when no active repository', async () => {377setupInternalUser();378379// Mock: no active repository380vi.spyOn(gitService.activeRepository, 'get').mockReturnValue(undefined);381382const repoTelemetry = new RepoInfoTelemetry(383'test-message-id',384telemetryService,385gitService,386gitDiffService,387gitExtensionService,388logService,389fileSystemService,390workspaceFileIndex,391configurationService,392copilotTokenStore393);394395await repoTelemetry.sendBeginTelemetryIfNeeded();396397// Assert: no telemetry sent398assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);399});400401test('should send telemetry with noChanges result when no changes from upstream', async () => {402setupInternalUser();403mockGitServiceWithRepository();404mockGitExtensionWithUpstream('abc123');405406// Mock: no changes from upstream407vi.spyOn(gitService, 'diffWith').mockResolvedValue([]);408409const repoTelemetry = new RepoInfoTelemetry(410'test-message-id',411telemetryService,412gitService,413gitDiffService,414gitExtensionService,415logService,416fileSystemService,417workspaceFileIndex,418configurationService,419copilotTokenStore420);421422await repoTelemetry.sendBeginTelemetryIfNeeded();423424// Assert: telemetry sent with noChanges result425assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);426const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];427assert.strictEqual(call[1].result, 'noChanges');428assert.strictEqual(call[1].diffsJSON, undefined);429assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');430assert.strictEqual(call[1].headCommitHash, 'abc123');431});432433test('should not send telemetry when no GitHub or ADO remote', async () => {434setupInternalUser();435436// Mock: repository with changes but no GitHub or ADO remote437vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({438rootUri: URI.file('/test/repo'),439changes: {440mergeChanges: [],441indexChanges: [],442workingTree: [],443untrackedChanges: []444},445remotes: [],446remoteFetchUrls: [],447upstreamRemote: undefined,448} as any);449450mockGitExtensionWithUpstream('abc123', 'https://gitlab.com/user/repo.git');451452const repoTelemetry = new RepoInfoTelemetry(453'test-message-id',454telemetryService,455gitService,456gitDiffService,457gitExtensionService,458logService,459fileSystemService,460workspaceFileIndex,461configurationService,462copilotTokenStore463);464465await repoTelemetry.sendBeginTelemetryIfNeeded();466467// Assert: no telemetry sent468assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);469});470471test('should send telemetry with correct repoType for Azure DevOps repository', async () => {472setupInternalUser();473474// Mock: ADO repository475vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({476rootUri: URI.file('/test/repo'),477changes: {478mergeChanges: [],479indexChanges: [],480workingTree: [{481uri: URI.file('/test/repo/file.ts'),482originalUri: URI.file('/test/repo/file.ts'),483renameUri: undefined,484status: Status.MODIFIED485}],486untrackedChanges: []487},488remotes: ['origin'],489remoteFetchUrls: ['https://dev.azure.com/myorg/myproject/_git/myrepo'],490upstreamRemote: 'origin',491headBranchName: 'main',492headCommitHash: 'abc123',493upstreamBranchName: 'origin/main',494isRebasing: false,495} as any);496497mockGitExtensionWithUpstream('abc123def456', 'https://dev.azure.com/myorg/myproject/_git/myrepo');498mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);499500const repoTelemetry = new RepoInfoTelemetry(501'test-message-id',502telemetryService,503gitService,504gitDiffService,505gitExtensionService,506logService,507fileSystemService,508workspaceFileIndex,509configurationService,510copilotTokenStore511);512513await repoTelemetry.sendBeginTelemetryIfNeeded();514515// Assert: telemetry sent with repoType = 'ado'516assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);517const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];518assert.strictEqual(call[0], 'request.repoInfo');519assert.strictEqual(call[1].repoType, 'ado');520assert.strictEqual(call[1].remoteUrl, 'https://dev.azure.com/myorg/myproject/_git/myrepo');521assert.strictEqual(call[1].headCommitHash, 'abc123def456');522assert.strictEqual(call[1].result, 'success');523});524525test('should normalize remote URL when logging telemetry', async () => {526setupInternalUser();527528// Mock: repository with SSH-style URL that needs normalization529const sshUrl = '[email protected]:microsoft/vscode.git';530vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({531rootUri: URI.file('/test/repo'),532changes: {533mergeChanges: [],534indexChanges: [],535workingTree: [{536uri: URI.file('/test/repo/file.ts'),537originalUri: URI.file('/test/repo/file.ts'),538renameUri: undefined,539status: Status.MODIFIED540}],541untrackedChanges: []542},543remotes: ['origin'],544remoteFetchUrls: [sshUrl],545upstreamRemote: 'origin',546headBranchName: 'main',547headCommitHash: 'abc123',548upstreamBranchName: 'origin/main',549isRebasing: false,550} as any);551552mockGitExtensionWithUpstream('abc123def456', sshUrl);553mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);554555const repoTelemetry = new RepoInfoTelemetry(556'test-message-id',557telemetryService,558gitService,559gitDiffService,560gitExtensionService,561logService,562fileSystemService,563workspaceFileIndex,564configurationService,565copilotTokenStore566);567568await repoTelemetry.sendBeginTelemetryIfNeeded();569570// Assert: URL is normalized to HTTPS571assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);572const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];573assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');574assert.notStrictEqual(call[1].remoteUrl, sshUrl);575});576577test('should not send telemetry when no upstream commit', async () => {578setupInternalUser();579mockGitServiceWithRepository();580581// Mock: no upstream commit582mockGitExtensionWithUpstream(undefined);583584const repoTelemetry = new RepoInfoTelemetry(585'test-message-id',586telemetryService,587gitService,588gitDiffService,589gitExtensionService,590logService,591fileSystemService,592workspaceFileIndex,593configurationService,594copilotTokenStore595);596597await repoTelemetry.sendBeginTelemetryIfNeeded();598599// Assert: no telemetry sent600assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);601});602603test('should send telemetry with valid GitHub repository', async () => {604setupInternalUser();605mockGitServiceWithRepository();606mockGitExtensionWithUpstream('abc123def456');607mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);608609const repoTelemetry = new RepoInfoTelemetry(610'test-message-id',611telemetryService,612gitService,613gitDiffService,614gitExtensionService,615logService,616fileSystemService,617workspaceFileIndex,618configurationService,619copilotTokenStore620);621622await repoTelemetry.sendBeginTelemetryIfNeeded();623624// Assert: telemetry sent with correct properties625assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);626const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];627assert.strictEqual(call[0], 'request.repoInfo');628assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');629assert.strictEqual(call[1].headCommitHash, 'abc123def456');630assert.strictEqual(call[1].result, 'success');631});632633// ========================================634// File System Watching Tests635// ========================================636637test('should detect file creation during diff', async () => {638setupInternalUser();639mockGitServiceWithRepository();640mockGitExtensionWithUpstream('abc123');641642// Mock git diff to trigger file change during execution643vi.spyOn(gitService, 'diffWith').mockImplementation(async () => {644// Simulate file creation during diff645mockWatcher.triggerCreate(URI.file('/test/repo/newfile.ts') as any);646647// Mock a change being returned from diffWith, we don't want to see this in the final telemetry648// instead we want to see the 'filesChanged' result due to the file system change649return [{650uri: URI.file('/test/repo/file.ts'),651originalUri: URI.file('/test/repo/file.ts'),652renameUri: undefined,653status: Status.MODIFIED654}] as any;655});656657const repoTelemetry = new RepoInfoTelemetry(658'test-message-id',659telemetryService,660gitService,661gitDiffService,662gitExtensionService,663logService,664fileSystemService,665workspaceFileIndex,666configurationService,667copilotTokenStore668);669670await repoTelemetry.sendBeginTelemetryIfNeeded();671672// Assert: filesChanged result673assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);674const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];675assert.strictEqual(call[1].result, 'filesChanged');676assert.strictEqual(call[1].diffsJSON, undefined);677});678679test('should detect file modification during diff', async () => {680setupInternalUser();681mockGitServiceWithRepository();682mockGitExtensionWithUpstream('abc123');683684// Mock git diff to trigger file change during execution685vi.spyOn(gitService, 'diffWith').mockImplementation(async () => {686// Simulate file modification during diff687mockWatcher.triggerChange(URI.file('/test/repo/file.ts') as any);688689// Mock a change being returned from diffWith, we don't want to see this in the final telemetry690// instead we want to see the 'filesChanged' result due to the file system change691return [{692uri: URI.file('/test/repo/file.ts'),693originalUri: URI.file('/test/repo/file.ts'),694renameUri: undefined,695status: Status.MODIFIED696}] as any;697});698699const repoTelemetry = new RepoInfoTelemetry(700'test-message-id',701telemetryService,702gitService,703gitDiffService,704gitExtensionService,705logService,706fileSystemService,707workspaceFileIndex,708configurationService,709copilotTokenStore710);711712await repoTelemetry.sendBeginTelemetryIfNeeded();713714// Assert: filesChanged result715assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);716const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];717assert.strictEqual(call[1].result, 'filesChanged');718assert.strictEqual(call[1].diffsJSON, undefined);719});720721test('should detect file deletion during diff', async () => {722setupInternalUser();723mockGitServiceWithRepository();724mockGitExtensionWithUpstream('abc123');725726// Mock git diff to trigger file change during execution727vi.spyOn(gitService, 'diffWith').mockImplementation(async () => {728// Simulate file deletion during diff729mockWatcher.triggerDelete(URI.file('/test/repo/oldfile.ts') as any);730731// Mock a change being returned from diffWith, we don't want to see this in the final telemetry732// instead we want to see the 'filesChanged' result due to the file system change733return [{734uri: URI.file('/test/repo/file.ts'),735originalUri: URI.file('/test/repo/file.ts'),736renameUri: undefined,737status: Status.MODIFIED738}] as any;739});740741const repoTelemetry = new RepoInfoTelemetry(742'test-message-id',743telemetryService,744gitService,745gitDiffService,746gitExtensionService,747logService,748fileSystemService,749workspaceFileIndex,750configurationService,751copilotTokenStore752);753754await repoTelemetry.sendBeginTelemetryIfNeeded();755756// Assert: filesChanged result757assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);758const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];759assert.strictEqual(call[1].result, 'filesChanged');760assert.strictEqual(call[1].diffsJSON, undefined);761});762763test('should detect file change during diff processing', async () => {764setupInternalUser();765mockGitServiceWithRepository();766mockGitExtensionWithUpstream('abc123');767768vi.spyOn(gitService, 'diffWith').mockResolvedValue([{769uri: URI.file('/test/repo/file.ts'),770originalUri: URI.file('/test/repo/file.ts'),771renameUri: undefined,772status: Status.MODIFIED773}] as any);774775// Mock git diff service to trigger file change during processing776vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockImplementation(async () => {777// Simulate file change during diff processing778mockWatcher.triggerChange(URI.file('/test/repo/file.ts') as any);779return [{780uri: URI.file('/test/repo/file.ts'),781originalUri: URI.file('/test/repo/file.ts'),782renameUri: undefined,783status: Status.MODIFIED,784diff: 'some diff content'785}];786});787788const repoTelemetry = new RepoInfoTelemetry(789'test-message-id',790telemetryService,791gitService,792gitDiffService,793gitExtensionService,794logService,795fileSystemService,796workspaceFileIndex,797configurationService,798copilotTokenStore799);800801await repoTelemetry.sendBeginTelemetryIfNeeded();802803// Assert: filesChanged result804assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);805const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];806assert.strictEqual(call[1].result, 'filesChanged');807assert.strictEqual(call[1].diffsJSON, undefined);808});809810test('should properly dispose file watcher', async () => {811setupInternalUser();812mockGitServiceWithRepository();813mockGitExtensionWithUpstream('abc123');814mockGitDiffService([]);815816const repoTelemetry = new RepoInfoTelemetry(817'test-message-id',818telemetryService,819gitService,820gitDiffService,821gitExtensionService,822logService,823fileSystemService,824workspaceFileIndex,825configurationService,826copilotTokenStore827);828829await repoTelemetry.sendBeginTelemetryIfNeeded();830831// Assert: watcher was disposed832assert.strictEqual(mockWatcher.isDisposed, true);833});834835// ========================================836// VFS / Sparse Checkout Tests837// ========================================838839test('should skip with virtualFileSystem result when core.virtualfilesystem is set', async () => {840setupInternalUser();841mockGitServiceWithRepository();842mockGitExtensionWithUpstream('abc123');843844// Override getConfig to return a hook path for core.virtualfilesystem (any non-empty string means VFS is active)845const mockApi = gitExtensionService.getExtensionApi();846const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;847vi.spyOn(mockRepo, 'getConfig').mockImplementation(async key => {848if (key === 'core.virtualfilesystem') {849return '/path/to/vfs-hook';850}851return '';852});853854const repoTelemetry = new RepoInfoTelemetry(855'test-message-id',856telemetryService,857gitService,858gitDiffService,859gitExtensionService,860logService,861fileSystemService,862workspaceFileIndex,863configurationService,864copilotTokenStore865);866867await repoTelemetry.sendBeginTelemetryIfNeeded();868869assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);870const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];871assert.strictEqual(call[1].result, 'virtualFileSystem');872assert.strictEqual(call[1].diffsJSON, undefined);873874// Ensure expensive diff operations were never called875assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);876});877878test('should skip with virtualFileSystem result when core.sparsecheckout is true', async () => {879setupInternalUser();880mockGitServiceWithRepository();881mockGitExtensionWithUpstream('abc123');882883const mockApi = gitExtensionService.getExtensionApi();884const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;885vi.spyOn(mockRepo, 'getConfig').mockImplementation(async key => {886if (key === 'core.sparsecheckout') {887return 'true';888}889return '';890});891892const repoTelemetry = new RepoInfoTelemetry(893'test-message-id',894telemetryService,895gitService,896gitDiffService,897gitExtensionService,898logService,899fileSystemService,900workspaceFileIndex,901configurationService,902copilotTokenStore903);904905await repoTelemetry.sendBeginTelemetryIfNeeded();906907assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);908const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];909assert.strictEqual(call[1].result, 'virtualFileSystem');910assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);911});912913test('should skip with virtualFileSystem result when getConfig throws', async () => {914setupInternalUser();915mockGitServiceWithRepository();916mockGitExtensionWithUpstream('abc123');917918const mockApi = gitExtensionService.getExtensionApi();919const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;920vi.spyOn(mockRepo, 'getConfig').mockRejectedValue(new Error('git config failed'));921922const repoTelemetry = new RepoInfoTelemetry(923'test-message-id',924telemetryService,925gitService,926gitDiffService,927gitExtensionService,928logService,929fileSystemService,930workspaceFileIndex,931configurationService,932copilotTokenStore933);934935await repoTelemetry.sendBeginTelemetryIfNeeded();936937assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);938const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];939assert.strictEqual(call[1].result, 'virtualFileSystem');940assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);941});942943// ========================================944// Commit Count Tests945// ========================================946947test('should skip with tooManyCommits result when commit count exceeds limit', async () => {948setupInternalUser();949mockGitServiceWithRepository();950mockGitExtensionWithUpstream('abc123');951952const mockApi = gitExtensionService.getExtensionApi();953const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;954// Return 30 commits (>= MAX_DIFF_COMMITS)955vi.spyOn(mockRepo, 'log').mockResolvedValue(956Array.from({ length: 30 }, (_, i) => ({ hash: `commit${i}`, message: `msg${i}` })) as any957);958959const repoTelemetry = new RepoInfoTelemetry(960'test-message-id',961telemetryService,962gitService,963gitDiffService,964gitExtensionService,965logService,966fileSystemService,967workspaceFileIndex,968configurationService,969copilotTokenStore970);971972await repoTelemetry.sendBeginTelemetryIfNeeded();973974assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);975const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];976assert.strictEqual(call[1].result, 'tooManyCommits');977assert.strictEqual(call[1].diffsJSON, undefined);978assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);979});980981test('should proceed normally when commit count is below limit', async () => {982setupInternalUser();983mockGitServiceWithRepository();984mockGitExtensionWithUpstream('abc123');985mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);986987const mockApi = gitExtensionService.getExtensionApi();988const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;989// Return 5 commits (below limit)990vi.spyOn(mockRepo, 'log').mockResolvedValue(991Array.from({ length: 5 }, (_, i) => ({ hash: `commit${i}`, message: `msg${i}` })) as any992);993994const repoTelemetry = new RepoInfoTelemetry(995'test-message-id',996telemetryService,997gitService,998gitDiffService,999gitExtensionService,1000logService,1001fileSystemService,1002workspaceFileIndex,1003configurationService,1004copilotTokenStore1005);10061007await repoTelemetry.sendBeginTelemetryIfNeeded();10081009assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1010const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1011assert.strictEqual(call[1].result, 'success');1012assert.ok(call[1].diffsJSON);1013});10141015test('should skip with tooManyCommits result when log throws', async () => {1016setupInternalUser();1017mockGitServiceWithRepository();1018mockGitExtensionWithUpstream('abc123');10191020const mockApi = gitExtensionService.getExtensionApi();1021const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;1022vi.spyOn(mockRepo, 'log').mockRejectedValue(new Error('git log failed'));10231024const repoTelemetry = new RepoInfoTelemetry(1025'test-message-id',1026telemetryService,1027gitService,1028gitDiffService,1029gitExtensionService,1030logService,1031fileSystemService,1032workspaceFileIndex,1033configurationService,1034copilotTokenStore1035);10361037await repoTelemetry.sendBeginTelemetryIfNeeded();10381039assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1040const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1041assert.strictEqual(call[1].result, 'tooManyCommits');1042assert.strictEqual((gitService.diffWith as any).mock.calls.length, 0);1043});10441045// ========================================1046// Diff Too Big Tests1047// ========================================10481049test('should detect when there are too many changes', async () => {1050setupInternalUser();1051mockGitServiceWithRepository();1052mockGitExtensionWithUpstream('abc123');10531054// Create 101 changes (exceeds MAX_CHANGES of 100)1055const manyChanges = Array.from({ length: 101 }, (_, i) => ({1056uri: URI.file(`/test/repo/file${i}.ts`),1057originalUri: URI.file(`/test/repo/file${i}.ts`),1058renameUri: undefined,1059status: Status.MODIFIED1060}));10611062vi.spyOn(gitService, 'diffWith').mockResolvedValue(manyChanges as any);10631064const repoTelemetry = new RepoInfoTelemetry(1065'test-message-id',1066telemetryService,1067gitService,1068gitDiffService,1069gitExtensionService,1070logService,1071fileSystemService,1072workspaceFileIndex,1073configurationService,1074copilotTokenStore1075);10761077await repoTelemetry.sendBeginTelemetryIfNeeded();10781079// Assert: tooManyChanges result1080assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1081const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1082assert.strictEqual(call[1].result, 'tooManyChanges');1083assert.strictEqual(call[1].diffsJSON, undefined);1084assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');1085assert.strictEqual(call[1].headCommitHash, 'abc123');1086});10871088test('should detect when diff is too large', async () => {1089setupInternalUser();1090mockGitServiceWithRepository();1091mockGitExtensionWithUpstream('abc123');10921093vi.spyOn(gitService, 'diffWith').mockResolvedValue([{1094uri: URI.file('/test/repo/file.ts'),1095originalUri: URI.file('/test/repo/file.ts'),1096renameUri: undefined,1097status: Status.MODIFIED1098}] as any);10991100// Create a diff that exceeds 900KB when serialized to JSON1101const largeDiff = 'x'.repeat(901 * 1024);1102vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{1103uri: URI.file('/test/repo/file.ts'),1104originalUri: URI.file('/test/repo/file.ts'),1105renameUri: undefined,1106status: Status.MODIFIED,1107diff: largeDiff1108}]);11091110const repoTelemetry = new RepoInfoTelemetry(1111'test-message-id',1112telemetryService,1113gitService,1114gitDiffService,1115gitExtensionService,1116logService,1117fileSystemService,1118workspaceFileIndex,1119configurationService,1120copilotTokenStore1121);11221123await repoTelemetry.sendBeginTelemetryIfNeeded();11241125// Assert: diffTooLarge result1126assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1127const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1128assert.strictEqual(call[1].result, 'diffTooLarge');1129assert.strictEqual(call[1].diffsJSON, undefined);1130assert.strictEqual(call[1].remoteUrl, 'https://github.com/microsoft/vscode.git');1131assert.strictEqual(call[1].headCommitHash, 'abc123');1132});11331134test('should send diff when within size limits', async () => {1135setupInternalUser();1136mockGitServiceWithRepository();1137mockGitExtensionWithUpstream('abc123');11381139vi.spyOn(gitService, 'diffWith').mockResolvedValue([{1140uri: URI.file('/test/repo/file.ts'),1141originalUri: URI.file('/test/repo/file.ts'),1142renameUri: undefined,1143status: Status.MODIFIED1144}] as any);11451146// Create a diff that is within limits1147const normalDiff = 'some normal diff content';1148vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{1149uri: URI.file('/test/repo/file.ts'),1150originalUri: URI.file('/test/repo/file.ts'),1151renameUri: undefined,1152status: Status.MODIFIED,1153diff: normalDiff1154}]);11551156const repoTelemetry = new RepoInfoTelemetry(1157'test-message-id',1158telemetryService,1159gitService,1160gitDiffService,1161gitExtensionService,1162logService,1163fileSystemService,1164workspaceFileIndex,1165configurationService,1166copilotTokenStore1167);11681169await repoTelemetry.sendBeginTelemetryIfNeeded();11701171// Assert: success with diff1172assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1173const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1174assert.strictEqual(call[1].result, 'success');1175assert.ok(call[1].diffsJSON);11761177const diffs = JSON.parse(call[1].diffsJSON);1178assert.strictEqual(diffs.length, 1);1179assert.strictEqual(diffs[0].diff, normalDiff);1180});11811182test('should handle multiple files in diff', async () => {1183setupInternalUser();1184mockGitServiceWithRepository();1185mockGitExtensionWithUpstream('abc123');11861187vi.spyOn(gitService, 'diffWith').mockResolvedValue([1188{1189uri: URI.file('/test/repo/file1.ts'),1190originalUri: URI.file('/test/repo/file1.ts'),1191renameUri: undefined,1192status: Status.MODIFIED1193},1194{1195uri: URI.file('/test/repo/file2.ts'),1196originalUri: URI.file('/test/repo/file2.ts'),1197renameUri: undefined,1198status: Status.INDEX_ADDED1199},1200{1201uri: URI.file('/test/repo/file3.ts'),1202originalUri: URI.file('/test/repo/file3.ts'),1203renameUri: undefined,1204status: Status.DELETED1205}1206] as any);12071208vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([1209{1210uri: URI.file('/test/repo/file1.ts'),1211originalUri: URI.file('/test/repo/file1.ts'),1212renameUri: undefined,1213status: Status.MODIFIED,1214diff: 'diff for file1'1215},1216{1217uri: URI.file('/test/repo/file2.ts'),1218originalUri: URI.file('/test/repo/file2.ts'),1219renameUri: undefined,1220status: Status.INDEX_ADDED,1221diff: 'diff for file2'1222},1223{1224uri: URI.file('/test/repo/file3.ts'),1225originalUri: URI.file('/test/repo/file3.ts'),1226renameUri: undefined,1227status: Status.DELETED,1228diff: 'diff for file3'1229}1230]);12311232const repoTelemetry = new RepoInfoTelemetry(1233'test-message-id',1234telemetryService,1235gitService,1236gitDiffService,1237gitExtensionService,1238logService,1239fileSystemService,1240workspaceFileIndex,1241configurationService,1242copilotTokenStore1243);12441245await repoTelemetry.sendBeginTelemetryIfNeeded();12461247// Assert: success with all diffs1248assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1249const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1250assert.strictEqual(call[1].result, 'success');12511252const diffs = JSON.parse(call[1].diffsJSON);1253assert.strictEqual(diffs.length, 3);1254assert.strictEqual(diffs[0].status, 'MODIFIED');1255assert.strictEqual(diffs[1].status, 'INDEX_ADDED');1256assert.strictEqual(diffs[2].status, 'DELETED');1257});12581259test('should handle renamed files in diff', async () => {1260setupInternalUser();1261mockGitServiceWithRepository();1262mockGitExtensionWithUpstream('abc123');12631264vi.spyOn(gitService, 'diffWith').mockResolvedValue([{1265uri: URI.file('/test/repo/newname.ts'),1266originalUri: URI.file('/test/repo/oldname.ts'),1267renameUri: URI.file('/test/repo/newname.ts'),1268status: Status.INDEX_RENAMED1269}] as any);12701271vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{1272uri: URI.file('/test/repo/newname.ts'),1273originalUri: URI.file('/test/repo/oldname.ts'),1274renameUri: URI.file('/test/repo/newname.ts'),1275status: Status.INDEX_RENAMED,1276diff: 'diff content'1277}]);12781279const repoTelemetry = new RepoInfoTelemetry(1280'test-message-id',1281telemetryService,1282gitService,1283gitDiffService,1284gitExtensionService,1285logService,1286fileSystemService,1287workspaceFileIndex,1288configurationService,1289copilotTokenStore1290);12911292await repoTelemetry.sendBeginTelemetryIfNeeded();12931294// Assert: success with rename info1295assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1296const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1297assert.strictEqual(call[1].result, 'success');12981299const diffs = JSON.parse(call[1].diffsJSON);1300assert.strictEqual(diffs.length, 1);1301assert.strictEqual(diffs[0].status, 'INDEX_RENAMED');1302assert.ok(diffs[0].renameUri);1303});13041305test('should include untracked files from both workingTreeChanges and untrackedChanges', async () => {1306setupInternalUser();1307mockGitServiceWithRepository();13081309// Mock git extension with untracked files in both workingTreeChanges and untrackedChanges1310const mockRepo = {1311getMergeBase: vi.fn(),1312getBranchBase: vi.fn(),1313getCommit: vi.fn(),1314getConfig: vi.fn().mockResolvedValue(''),1315log: vi.fn().mockResolvedValue([]),1316state: {1317HEAD: {1318upstream: {1319commit: 'abc123',1320remote: 'origin',1321},1322},1323remotes: [{1324name: 'origin',1325fetchUrl: 'https://github.com/microsoft/vscode.git',1326pushUrl: 'https://github.com/microsoft/vscode.git',1327isReadOnly: false,1328}],1329workingTreeChanges: [{1330uri: URI.file('/test/repo/filea.txt'),1331originalUri: URI.file('/test/repo/filea.txt'),1332renameUri: undefined,1333status: Status.UNTRACKED1334}],1335untrackedChanges: [{1336uri: URI.file('/test/repo/fileb.txt'),1337originalUri: URI.file('/test/repo/fileb.txt'),1338renameUri: undefined,1339status: Status.UNTRACKED1340}],1341},1342};13431344mockRepo.getCommit.mockResolvedValue({1345hash: 'abc123',1346message: 'test commit',1347commitDate: new Date(),1348});13491350mockRepo.getMergeBase.mockImplementation(async (ref1: string, ref2: string) => {1351if (ref1 === 'HEAD' && ref2 === '@{upstream}') {1352return 'abc123';1353}1354return undefined;1355});13561357mockRepo.getBranchBase.mockResolvedValue(undefined);13581359const mockApi = {1360getRepository: () => mockRepo,1361};1362vi.spyOn(gitExtensionService, 'getExtensionApi').mockReturnValue(mockApi as any);13631364// Mock diffWith to return one modified file1365vi.spyOn(gitService, 'diffWith').mockResolvedValue([{1366uri: URI.file('/test/repo/modified.ts'),1367originalUri: URI.file('/test/repo/modified.ts'),1368renameUri: undefined,1369status: Status.MODIFIED1370}] as any);13711372// Mock diff service to return all three files1373vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([1374{1375uri: URI.file('/test/repo/modified.ts'),1376originalUri: URI.file('/test/repo/modified.ts'),1377renameUri: undefined,1378status: Status.MODIFIED,1379diff: 'modified content'1380},1381{1382uri: URI.file('/test/repo/filea.txt'),1383originalUri: URI.file('/test/repo/filea.txt'),1384renameUri: undefined,1385status: Status.UNTRACKED,1386diff: 'new file a'1387},1388{1389uri: URI.file('/test/repo/fileb.txt'),1390originalUri: URI.file('/test/repo/fileb.txt'),1391renameUri: undefined,1392status: Status.UNTRACKED,1393diff: 'new file b'1394}1395]);13961397const repoTelemetry = new RepoInfoTelemetry(1398'test-message-id',1399telemetryService,1400gitService,1401gitDiffService,1402gitExtensionService,1403logService,1404fileSystemService,1405workspaceFileIndex,1406configurationService,1407copilotTokenStore1408);14091410await repoTelemetry.sendBeginTelemetryIfNeeded();14111412// Assert: success with all three files in telemetry1413assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1414const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1415assert.strictEqual(call[1].result, 'success');14161417const diffs = JSON.parse(call[1].diffsJSON);1418assert.strictEqual(diffs.length, 3, 'Should include 1 modified file + 2 untracked files');14191420// Verify all three files are present1421const uris = diffs.map((d: any) => d.uri);1422assert.ok(uris.includes('file:///test/repo/modified.ts'), 'Should include modified file');1423assert.ok(uris.includes('file:///test/repo/filea.txt'), 'Should include filea.txt from workingTreeChanges');1424assert.ok(uris.includes('file:///test/repo/fileb.txt'), 'Should include fileb.txt from untrackedChanges');14251426// Verify statuses1427const fileaEntry = diffs.find((d: any) => d.uri === 'file:///test/repo/filea.txt');1428const filebEntry = diffs.find((d: any) => d.uri === 'file:///test/repo/fileb.txt');1429assert.strictEqual(fileaEntry.status, 'UNTRACKED');1430assert.strictEqual(filebEntry.status, 'UNTRACKED');1431});14321433// ========================================1434// Measurements Tests1435// ========================================14361437test('should include workspaceFileCount in measurements', async () => {1438setupInternalUser();1439mockGitServiceWithRepository();1440mockGitExtensionWithUpstream('abc123');1441mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);14421443// Set a specific file count1444(workspaceFileIndex as any).fileCount = 250;14451446const repoTelemetry = new RepoInfoTelemetry(1447'test-message-id',1448telemetryService,1449gitService,1450gitDiffService,1451gitExtensionService,1452logService,1453fileSystemService,1454workspaceFileIndex,1455configurationService,1456copilotTokenStore1457);14581459await repoTelemetry.sendBeginTelemetryIfNeeded();14601461// Assert: measurements contain workspaceFileCount1462assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1463const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1464assert.ok(call[2], 'measurements parameter should exist');1465assert.strictEqual(call[2].workspaceFileCount, 250);1466});14671468test('should include changedFileCount in measurements', async () => {1469setupInternalUser();1470mockGitServiceWithRepository();1471mockGitExtensionWithUpstream('abc123');14721473// Mock 5 changes1474const changes = Array.from({ length: 5 }, (_, i) => ({1475uri: URI.file(`/test/repo/file${i}.ts`),1476originalUri: URI.file(`/test/repo/file${i}.ts`),1477renameUri: undefined,1478status: Status.MODIFIED1479}));14801481vi.spyOn(gitService, 'diffWith').mockResolvedValue(changes as any);14821483vi.spyOn(gitDiffService, 'getChangeDiffs').mockResolvedValue(1484changes.map((c, i) => ({1485uri: URI.file(`/test/repo/file${i}.ts`),1486originalUri: URI.file(`/test/repo/file${i}.ts`),1487renameUri: undefined,1488status: Status.MODIFIED,1489diff: `diff for file${i}`1490}))1491);14921493const repoTelemetry = new RepoInfoTelemetry(1494'test-message-id',1495telemetryService,1496gitService,1497gitDiffService,1498gitExtensionService,1499logService,1500fileSystemService,1501workspaceFileIndex,1502configurationService,1503copilotTokenStore1504);15051506await repoTelemetry.sendBeginTelemetryIfNeeded();15071508// Assert: measurements contain changedFileCount1509assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1510const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1511assert.ok(call[2], 'measurements parameter should exist');1512assert.strictEqual(call[2].changedFileCount, 5);1513});15141515test('should set changedFileCount to 0 when no changes', async () => {1516setupInternalUser();1517mockGitServiceWithRepository();1518mockGitExtensionWithUpstream('abc123');15191520// Mock: no changes from upstream1521vi.spyOn(gitService, 'diffWith').mockResolvedValue([]);15221523const repoTelemetry = new RepoInfoTelemetry(1524'test-message-id',1525telemetryService,1526gitService,1527gitDiffService,1528gitExtensionService,1529logService,1530fileSystemService,1531workspaceFileIndex,1532configurationService,1533copilotTokenStore1534);15351536await repoTelemetry.sendBeginTelemetryIfNeeded();15371538// Assert: changedFileCount is 01539assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1540const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1541assert.ok(call[2], 'measurements parameter should exist');1542assert.strictEqual(call[2].changedFileCount, 0);1543});15441545test('should include measurements in both begin and end telemetry', async () => {1546setupInternalUser();1547mockGitServiceWithRepository();1548mockGitExtensionWithUpstream('abc123');1549mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);15501551(workspaceFileIndex as any).fileCount = 150;15521553const repoTelemetry = new RepoInfoTelemetry(1554'test-message-id',1555telemetryService,1556gitService,1557gitDiffService,1558gitExtensionService,1559logService,1560fileSystemService,1561workspaceFileIndex,1562configurationService,1563copilotTokenStore1564);15651566await repoTelemetry.sendBeginTelemetryIfNeeded();1567await repoTelemetry.sendEndTelemetry();15681569// Assert: both begin and end have measurements1570assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 2);15711572const beginCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1573assert.ok(beginCall[2], 'begin measurements should exist');1574assert.strictEqual(beginCall[2].workspaceFileCount, 150);1575assert.strictEqual(beginCall[2].changedFileCount, 1);15761577const endCall = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[1];1578assert.ok(endCall[2], 'end measurements should exist');1579assert.strictEqual(endCall[2].workspaceFileCount, 150);1580assert.strictEqual(endCall[2].changedFileCount, 1);1581});15821583test('should include measurements even when diff is too large', async () => {1584setupInternalUser();1585mockGitServiceWithRepository();1586mockGitExtensionWithUpstream('abc123');15871588vi.spyOn(gitService, 'diffWith').mockResolvedValue([{1589uri: URI.file('/test/repo/file.ts'),1590originalUri: URI.file('/test/repo/file.ts'),1591renameUri: undefined,1592status: Status.MODIFIED1593}] as any);15941595// Create a diff that exceeds 900KB when serialized to JSON1596const largeDiff = 'x'.repeat(901 * 1024);1597vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue([{1598uri: URI.file('/test/repo/file.ts'),1599originalUri: URI.file('/test/repo/file.ts'),1600renameUri: undefined,1601status: Status.MODIFIED,1602diff: largeDiff1603}]);16041605(workspaceFileIndex as any).fileCount = 200;16061607const repoTelemetry = new RepoInfoTelemetry(1608'test-message-id',1609telemetryService,1610gitService,1611gitDiffService,1612gitExtensionService,1613logService,1614fileSystemService,1615workspaceFileIndex,1616configurationService,1617copilotTokenStore1618);16191620await repoTelemetry.sendBeginTelemetryIfNeeded();16211622// Assert: diffTooLarge result but measurements still present1623assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1624const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1625assert.strictEqual(call[1].result, 'diffTooLarge');1626assert.ok(call[2], 'measurements should still be present');1627assert.strictEqual(call[2].workspaceFileCount, 200);1628assert.strictEqual(call[2].changedFileCount, 1);1629});16301631test('should include measurements when there are too many changes', async () => {1632setupInternalUser();1633mockGitServiceWithRepository();1634mockGitExtensionWithUpstream('abc123');16351636// Create 101 changes (exceeds MAX_CHANGES of 100)1637const manyChanges = Array.from({ length: 101 }, (_, i) => ({1638uri: URI.file(`/test/repo/file${i}.ts`),1639originalUri: URI.file(`/test/repo/file${i}.ts`),1640renameUri: undefined,1641status: Status.MODIFIED1642}));16431644vi.spyOn(gitService, 'diffWith').mockResolvedValue(manyChanges as any);16451646(workspaceFileIndex as any).fileCount = 300;16471648const repoTelemetry = new RepoInfoTelemetry(1649'test-message-id',1650telemetryService,1651gitService,1652gitDiffService,1653gitExtensionService,1654logService,1655fileSystemService,1656workspaceFileIndex,1657configurationService,1658copilotTokenStore1659);16601661await repoTelemetry.sendBeginTelemetryIfNeeded();16621663// Assert: tooManyChanges result but measurements still present1664assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1665const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1666assert.strictEqual(call[1].result, 'tooManyChanges');1667assert.ok(call[2], 'measurements should still be present');1668assert.strictEqual(call[2].workspaceFileCount, 300);1669assert.strictEqual(call[2].changedFileCount, 101);1670});16711672test('should include diffSizeBytes in measurements when diffs are present', async () => {1673setupInternalUser();1674mockGitServiceWithRepository();1675mockGitExtensionWithUpstream('abc123');16761677const 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';1678mockGitDiffService([{ uri: '/test/repo/file.ts', diff: testDiff }]);16791680const repoTelemetry = new RepoInfoTelemetry(1681'test-message-id',1682telemetryService,1683gitService,1684gitDiffService,1685gitExtensionService,1686logService,1687fileSystemService,1688workspaceFileIndex,1689configurationService,1690copilotTokenStore1691);16921693await repoTelemetry.sendBeginTelemetryIfNeeded();16941695// Assert: diffSizeBytes measurement is set1696assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1697const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1698assert.strictEqual(call[1].result, 'success');1699assert.ok(call[2], 'measurements parameter should be present');1700assert.strictEqual(typeof call[2].diffSizeBytes, 'number');1701assert.ok(call[2].diffSizeBytes > 0, 'diffSizeBytes should be greater than 0');17021703// Calculate expected size from the mock data1704const expectedDiffsJSON = JSON.stringify([{1705uri: 'file:///test/repo/file.ts',1706originalUri: 'file:///test/repo/file.ts',1707renameUri: undefined,1708status: 'MODIFIED',1709diff: testDiff1710}]);1711const expectedSize = Buffer.byteLength(expectedDiffsJSON, 'utf8');1712assert.strictEqual(call[2].diffSizeBytes, expectedSize);1713});17141715// ========================================1716// Error Handling Tests1717// ========================================17181719test('should handle errors during git diff gracefully', async () => {1720setupInternalUser();1721mockGitServiceWithRepository();1722mockGitExtensionWithUpstream('abc123');17231724// Mock git diff to throw error1725vi.spyOn(gitService, 'diffWith').mockRejectedValue(new Error('Git error'));17261727const repoTelemetry = new RepoInfoTelemetry(1728'test-message-id',1729telemetryService,1730gitService,1731gitDiffService,1732gitExtensionService,1733logService,1734fileSystemService,1735workspaceFileIndex,1736configurationService,1737copilotTokenStore1738);17391740// Should not throw1741await repoTelemetry.sendBeginTelemetryIfNeeded();17421743// Assert: no telemetry sent due to error1744assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);1745});17461747test('should handle errors during diff processing gracefully', async () => {1748setupInternalUser();1749mockGitServiceWithRepository();1750mockGitExtensionWithUpstream('abc123');17511752vi.spyOn(gitService, 'diffWith').mockResolvedValue([{1753uri: URI.file('/test/repo/file.ts'),1754originalUri: URI.file('/test/repo/file.ts'),1755renameUri: undefined,1756status: Status.MODIFIED1757}] as any);17581759// Mock diff service to throw error1760vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockRejectedValue(new Error('Diff processing error'));17611762const repoTelemetry = new RepoInfoTelemetry(1763'test-message-id',1764telemetryService,1765gitService,1766gitDiffService,1767gitExtensionService,1768logService,1769fileSystemService,1770workspaceFileIndex,1771configurationService,1772copilotTokenStore1773);17741775// Should not throw1776await repoTelemetry.sendBeginTelemetryIfNeeded();17771778// Assert: no telemetry sent due to error1779assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);1780});17811782// ========================================1783// Disable Setting and Merge Base Age Tests1784// ========================================17851786test('should skip telemetry when disableRepoInfoTelemetry setting is enabled', async () => {1787setupInternalUser();1788mockGitServiceWithRepository();1789mockGitExtensionWithUpstream('abc123');1790mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);17911792// Enable the disable setting1793(configurationService as InMemoryConfigurationService).setConfig(1794ConfigKey.TeamInternal.DisableRepoInfoTelemetry, true1795);17961797const repoTelemetry = new RepoInfoTelemetry(1798'test-message-id',1799telemetryService,1800gitService,1801gitDiffService,1802gitExtensionService,1803logService,1804fileSystemService,1805workspaceFileIndex,1806configurationService,1807copilotTokenStore1808);18091810await repoTelemetry.sendBeginTelemetryIfNeeded();18111812// Assert: no telemetry sent1813assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 0);1814});18151816test('should return mergeBaseTooOld when upstream commit is older than 30 days', async () => {1817setupInternalUser();1818mockGitServiceWithRepository();1819mockGitExtensionWithUpstream('old-commit-abc');1820mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);18211822// Override getCommit to return a commit older than 30 days1823const mockApi = gitExtensionService.getExtensionApi();1824const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;1825(mockRepo as any).getCommit.mockResolvedValue({1826hash: 'old-commit-abc',1827message: 'old commit',1828commitDate: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000), // 45 days ago1829});18301831const repoTelemetry = new RepoInfoTelemetry(1832'test-message-id',1833telemetryService,1834gitService,1835gitDiffService,1836gitExtensionService,1837logService,1838fileSystemService,1839workspaceFileIndex,1840configurationService,1841copilotTokenStore1842);18431844await repoTelemetry.sendBeginTelemetryIfNeeded();18451846// Assert: telemetry sent with mergeBaseTooOld result1847assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1848const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1849assert.strictEqual(call[1].result, 'mergeBaseTooOld');1850assert.strictEqual(call[1].diffsJSON, undefined);1851});18521853test('should proceed normally when upstream commit is within 30 days', async () => {1854setupInternalUser();1855mockGitServiceWithRepository();1856mockGitExtensionWithUpstream('recent-commit');1857mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);18581859// getCommit already returns a recent commit by default in the mock18601861const repoTelemetry = new RepoInfoTelemetry(1862'test-message-id',1863telemetryService,1864gitService,1865gitDiffService,1866gitExtensionService,1867logService,1868fileSystemService,1869workspaceFileIndex,1870configurationService,1871copilotTokenStore1872);18731874await repoTelemetry.sendBeginTelemetryIfNeeded();18751876// Assert: telemetry sent with success result (not mergeBaseTooOld)1877assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1878const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1879assert.strictEqual(call[1].result, 'success');1880});18811882test('should return mergeBaseTooOld when getCommit fails', async () => {1883setupInternalUser();1884mockGitServiceWithRepository();1885mockGitExtensionWithUpstream('abc123');1886mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);18871888// Override getCommit to throw1889const mockApi = gitExtensionService.getExtensionApi();1890const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;1891(mockRepo as any).getCommit.mockRejectedValue(new Error('Failed to get commit'));18921893const repoTelemetry = new RepoInfoTelemetry(1894'test-message-id',1895telemetryService,1896gitService,1897gitDiffService,1898gitExtensionService,1899logService,1900fileSystemService,1901workspaceFileIndex,1902configurationService,1903copilotTokenStore1904);19051906await repoTelemetry.sendBeginTelemetryIfNeeded();19071908// Assert: telemetry sent with mergeBaseTooOld result1909assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1910const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1911assert.strictEqual(call[1].result, 'mergeBaseTooOld');1912});19131914test('should return mergeBaseTooOld when commit date is undefined', async () => {1915setupInternalUser();1916mockGitServiceWithRepository();1917mockGitExtensionWithUpstream('abc123');1918mockGitDiffService([{ uri: '/test/repo/file.ts', diff: 'some diff' }]);19191920// Override getCommit to return a commit without a date1921const mockApi = gitExtensionService.getExtensionApi();1922const mockRepo = mockApi!.getRepository(URI.file('/test/repo'))!;1923(mockRepo as any).getCommit.mockResolvedValue({1924hash: 'abc123',1925message: 'commit without date',1926commitDate: undefined,1927});19281929const repoTelemetry = new RepoInfoTelemetry(1930'test-message-id',1931telemetryService,1932gitService,1933gitDiffService,1934gitExtensionService,1935logService,1936fileSystemService,1937workspaceFileIndex,1938configurationService,1939copilotTokenStore1940);19411942await repoTelemetry.sendBeginTelemetryIfNeeded();19431944// Assert: telemetry sent with mergeBaseTooOld result1945assert.strictEqual((telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls.length, 1);1946const call = (telemetryService.sendInternalMSFTTelemetryEvent as any).mock.calls[0];1947assert.strictEqual(call[1].result, 'mergeBaseTooOld');1948});19491950// ========================================1951// Helper Functions1952// ========================================19531954function setupInternalUser() {1955const internalToken = new CopilotToken(createTestExtendedTokenInfo({1956token: 'tid=test;rt=1',1957sku: 'free_limited_copilot',1958expires_at: 9999999999,1959refresh_in: 180000,1960organization_list: ['4535c7beffc844b46bb1ed4aa04d759a'], // GitHub org for internal users1961isVscodeTeamMember: true,1962username: 'testUser',1963copilot_plan: 'unknown',1964}));1965copilotTokenStore.copilotToken = internalToken;1966}19671968function mockGitServiceWithRepository() {1969vi.spyOn(gitService.activeRepository, 'get').mockReturnValue({1970rootUri: URI.file('/test/repo'),1971changes: {1972mergeChanges: [],1973indexChanges: [],1974workingTree: [{1975uri: URI.file('/test/repo/file.ts'),1976originalUri: URI.file('/test/repo/file.ts'),1977renameUri: undefined,1978status: Status.MODIFIED1979}],1980untrackedChanges: []1981},1982remotes: ['origin'],1983remoteFetchUrls: ['https://github.com/microsoft/vscode.git'],1984upstreamRemote: 'origin',1985headBranchName: 'main',1986headCommitHash: 'abc123',1987upstreamBranchName: 'origin/main',1988isRebasing: false,1989} as any);1990}19911992function mockGitExtensionWithUpstream(upstreamCommit: string | undefined, remoteUrl: string = 'https://github.com/microsoft/vscode.git') {1993const mockRepo = {1994getMergeBase: vi.fn(),1995getBranchBase: vi.fn(),1996getCommit: vi.fn(),1997getConfig: vi.fn().mockResolvedValue(''),1998log: vi.fn().mockResolvedValue([]),1999state: {2000HEAD: {2001upstream: upstreamCommit ? {2002commit: upstreamCommit,2003remote: 'origin',2004} : undefined,2005},2006remotes: [{2007name: 'origin',2008fetchUrl: remoteUrl,2009pushUrl: remoteUrl,2010isReadOnly: false,2011}],2012workingTreeChanges: [],2013untrackedChanges: [],2014},2015};20162017// Set up getMergeBase to return upstreamCommit when called with 'HEAD' and '@upstream'2018mockRepo.getMergeBase.mockImplementation(async (ref1: string, ref2: string) => {2019if (ref1 === 'HEAD' && ref2 === '@{upstream}') {2020return upstreamCommit;2021}2022return undefined;2023});20242025// Set up getBranchBase to return undefined by default2026mockRepo.getBranchBase.mockResolvedValue(undefined);20272028// Set up getCommit to return a recent commit by default2029mockRepo.getCommit.mockResolvedValue({2030hash: upstreamCommit ?? 'abc123',2031message: 'test commit',2032commitDate: new Date(),2033});20342035const mockApi = {2036getRepository: () => mockRepo,2037};2038vi.spyOn(gitExtensionService, 'getExtensionApi').mockReturnValue(mockApi as any);2039}20402041function mockGitDiffService(diffs: any[]) {2042// Mock diffWith to return Change objects2043const changes = diffs.map(d => ({2044uri: URI.file(d.uri || '/test/repo/file.ts'),2045originalUri: URI.file(d.originalUri || d.uri || '/test/repo/file.ts'),2046renameUri: d.renameUri ? URI.file(d.renameUri) : undefined,2047status: d.status || Status.MODIFIED2048}));20492050vi.spyOn(gitService, 'diffWith').mockResolvedValue(2051diffs.length > 0 ? changes as any : []2052);20532054// Mock getWorkingTreeDiffsFromRef to return Diff objects (Change + diff property)2055vi.spyOn(gitDiffService, 'getWorkingTreeDiffsFromRef').mockResolvedValue(2056diffs.map(d => ({2057uri: URI.file(d.uri || '/test/repo/file.ts'),2058originalUri: URI.file(d.originalUri || d.uri || '/test/repo/file.ts'),2059renameUri: d.renameUri ? URI.file(d.renameUri) : undefined,2060status: d.status || Status.MODIFIED,2061diff: d.diff || 'test diff'2062}))2063);2064}2065});20662067// ========================================2068// Mock File System Watcher2069// ========================================20702071class MockFileSystemWatcher implements FileSystemWatcher {2072private _createHandlers: ((e: Uri) => any)[] = [];2073private _changeHandlers: ((e: Uri) => any)[] = [];2074private _deleteHandlers: ((e: Uri) => any)[] = [];2075public isDisposed = false;2076public ignoreCreateEvents = false;2077public ignoreChangeEvents = false;2078public ignoreDeleteEvents = false;20792080get onDidCreate(): Event<Uri> {2081return (listener) => {2082this._createHandlers.push(listener);2083return {2084dispose: () => {2085const index = this._createHandlers.indexOf(listener);2086if (index > -1) {2087this._createHandlers.splice(index, 1);2088}2089}2090};2091};2092}20932094get onDidChange(): Event<Uri> {2095return (listener) => {2096this._changeHandlers.push(listener);2097return {2098dispose: () => {2099const index = this._changeHandlers.indexOf(listener);2100if (index > -1) {2101this._changeHandlers.splice(index, 1);2102}2103}2104};2105};2106}21072108get onDidDelete(): Event<Uri> {2109return (listener) => {2110this._deleteHandlers.push(listener);2111return {2112dispose: () => {2113const index = this._deleteHandlers.indexOf(listener);2114if (index > -1) {2115this._deleteHandlers.splice(index, 1);2116}2117}2118};2119};2120}21212122triggerCreate(uri: Uri): void {2123this._createHandlers.forEach(h => h(uri));2124}21252126triggerChange(uri: Uri): void {2127this._changeHandlers.forEach(h => h(uri));2128}21292130triggerDelete(uri: Uri): void {2131this._deleteHandlers.forEach(h => h(uri));2132}21332134dispose(): void {2135this.isDisposed = true;2136this._createHandlers = [];2137this._changeHandlers = [];2138this._deleteHandlers = [];2139}2140}214121422143