Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/test/pullRequestDetectionService.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 { beforeEach, describe, expect, it, vi } from 'vitest';
7
import { IGitService, RepoContext } from '../../../../platform/git/common/gitService';
8
import { PullRequestSearchItem } from '../../../../platform/github/common/githubAPI';
9
import { IOctoKitService } from '../../../../platform/github/common/githubService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { mock } from '../../../../util/common/test/simpleMock';
12
import { Event } from '../../../../util/vs/base/common/event';
13
import { URI } from '../../../../util/vs/base/common/uri';
14
import { ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../../common/chatSessionWorktreeService';
15
import { PullRequestDetectionService } from '../pullRequestDetectionService';
16
17
class TestWorktreeService extends mock<IChatSessionWorktreeService>() {
18
declare readonly _serviceBrand: undefined;
19
override getWorktreeProperties = vi.fn(async (): Promise<ChatSessionWorktreeProperties | undefined> => undefined);
20
override setWorktreeProperties = vi.fn(async () => { });
21
}
22
23
class TestGitService extends mock<IGitService>() {
24
declare readonly _serviceBrand: undefined;
25
override onDidOpenRepository = Event.None;
26
override onDidCloseRepository = Event.None;
27
override onDidFinishInitialization = Event.None;
28
override activeRepository = { get: () => undefined } as IGitService['activeRepository'];
29
override repositories: RepoContext[] = [];
30
override getRepository = vi.fn(async (): Promise<RepoContext | undefined> => this.repositories[0]);
31
32
setRepo(repo: RepoContext): void {
33
this.repositories = [repo];
34
}
35
}
36
37
class TestOctoKitService extends mock<IOctoKitService>() {
38
declare readonly _serviceBrand: undefined;
39
override findPullRequestByHeadBranch = vi.fn(async (): Promise<PullRequestSearchItem | undefined> => undefined);
40
}
41
42
class TestLogService extends mock<ILogService>() {
43
declare readonly _serviceBrand: undefined;
44
override trace = vi.fn();
45
override debug = vi.fn();
46
override error = vi.fn();
47
}
48
49
function createV2WorktreeProperties(overrides?: Partial<ChatSessionWorktreeProperties>): ChatSessionWorktreeProperties {
50
return {
51
version: 2,
52
baseCommit: 'abc123',
53
baseBranchName: 'main',
54
branchName: 'copilot/test-branch',
55
repositoryPath: '/repo',
56
worktreePath: '/worktree',
57
...overrides,
58
} as ChatSessionWorktreeProperties;
59
}
60
61
function createPrSearchItem(overrides?: Partial<PullRequestSearchItem>): PullRequestSearchItem {
62
return {
63
id: 'pr-42',
64
number: 42,
65
title: 'Test PR',
66
url: 'https://github.com/owner/repo/pull/42',
67
state: 'OPEN',
68
isDraft: false,
69
createdAt: '2026-01-01T00:00:00Z',
70
updatedAt: '2026-01-01T00:00:00Z',
71
author: { login: 'user' },
72
repository: { owner: { login: 'owner' }, name: 'repo' },
73
additions: 1,
74
deletions: 0,
75
files: { totalCount: 1 },
76
fullDatabaseId: 42,
77
headRefOid: 'deadbeef',
78
headRefName: 'copilot/test-branch',
79
baseRefName: 'main',
80
body: '',
81
...overrides,
82
};
83
}
84
85
function createGitRepo(path: string = '/repo'): RepoContext {
86
return {
87
rootUri: URI.file(path),
88
kind: 'repository',
89
remotes: ['origin'],
90
remoteFetchUrls: ['https://github.com/owner/repo.git'],
91
} as unknown as RepoContext;
92
}
93
94
describe('PullRequestDetectionService', () => {
95
let worktreeService: TestWorktreeService;
96
let gitService: TestGitService;
97
let octoKitService: TestOctoKitService;
98
let logService: TestLogService;
99
let service: PullRequestDetectionService;
100
101
beforeEach(() => {
102
vi.restoreAllMocks();
103
worktreeService = new TestWorktreeService();
104
gitService = new TestGitService();
105
octoKitService = new TestOctoKitService();
106
logService = new TestLogService();
107
service = new PullRequestDetectionService(worktreeService, gitService, octoKitService, logService);
108
});
109
110
describe('detectPullRequest', () => {
111
it('does not query GitHub API when no worktree properties exist', async () => {
112
worktreeService.getWorktreeProperties.mockResolvedValue(undefined);
113
service.detectPullRequest('session-1');
114
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
115
expect(octoKitService.findPullRequestByHeadBranch).not.toHaveBeenCalled();
116
});
117
118
it('does not query GitHub API when version is not 2', async () => {
119
worktreeService.getWorktreeProperties.mockResolvedValue({
120
version: 1,
121
autoCommit: true,
122
baseCommit: 'abc',
123
branchName: 'branch',
124
repositoryPath: '/repo',
125
worktreePath: '/wt',
126
});
127
service.detectPullRequest('session-1');
128
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
129
expect(octoKitService.findPullRequestByHeadBranch).not.toHaveBeenCalled();
130
});
131
132
it('skips detection when pullRequestState is merged', async () => {
133
worktreeService.getWorktreeProperties.mockResolvedValue(
134
createV2WorktreeProperties({ pullRequestState: 'merged' })
135
);
136
service.detectPullRequest('session-1');
137
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
138
expect(octoKitService.findPullRequestByHeadBranch).not.toHaveBeenCalled();
139
});
140
141
it('skips detection when branchName is missing', async () => {
142
worktreeService.getWorktreeProperties.mockResolvedValue(
143
createV2WorktreeProperties({ branchName: '' })
144
);
145
service.detectPullRequest('session-1');
146
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
147
});
148
149
it('skips detection when repositoryPath is missing', async () => {
150
worktreeService.getWorktreeProperties.mockResolvedValue(
151
createV2WorktreeProperties({ repositoryPath: '' })
152
);
153
service.detectPullRequest('session-1');
154
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
155
});
156
157
it('updates properties when PR is found', async () => {
158
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
159
gitService.setRepo(createGitRepo());
160
octoKitService.findPullRequestByHeadBranch.mockResolvedValue(createPrSearchItem());
161
162
service.detectPullRequest('session-1');
163
await vi.waitFor(() => expect(worktreeService.setWorktreeProperties).toHaveBeenCalledWith(
164
'session-1',
165
expect.objectContaining({
166
pullRequestUrl: 'https://github.com/owner/repo/pull/42',
167
pullRequestState: 'open',
168
}),
169
));
170
});
171
172
it('fires onDidDetectPullRequest when PR is found on session open', async () => {
173
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
174
gitService.setRepo(createGitRepo());
175
octoKitService.findPullRequestByHeadBranch.mockResolvedValue(createPrSearchItem());
176
177
const firedSessionIds: string[] = [];
178
service.onDidDetectPullRequest(id => firedSessionIds.push(id));
179
180
service.detectPullRequest('session-1');
181
await vi.waitFor(() => expect(firedSessionIds).toEqual(['session-1']));
182
});
183
184
it('does not fire onDidDetectPullRequest when no PR found on session open', async () => {
185
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
186
gitService.setRepo(createGitRepo());
187
octoKitService.findPullRequestByHeadBranch.mockResolvedValue(undefined);
188
189
const firedSessionIds: string[] = [];
190
service.onDidDetectPullRequest(id => firedSessionIds.push(id));
191
192
service.detectPullRequest('session-1');
193
await vi.waitFor(() => expect(octoKitService.findPullRequestByHeadBranch).toHaveBeenCalled());
194
expect(firedSessionIds).toEqual([]);
195
});
196
197
it('does not update properties when PR url and state are unchanged', async () => {
198
worktreeService.getWorktreeProperties.mockResolvedValue(
199
createV2WorktreeProperties({
200
pullRequestUrl: 'https://github.com/owner/repo/pull/42',
201
pullRequestState: 'open',
202
})
203
);
204
gitService.setRepo(createGitRepo());
205
octoKitService.findPullRequestByHeadBranch.mockResolvedValue(createPrSearchItem());
206
207
service.detectPullRequest('session-1');
208
await vi.waitFor(() => expect(octoKitService.findPullRequestByHeadBranch).toHaveBeenCalled());
209
expect(worktreeService.setWorktreeProperties).not.toHaveBeenCalled();
210
});
211
212
it('updates properties when PR state changed', async () => {
213
worktreeService.getWorktreeProperties.mockResolvedValue(
214
createV2WorktreeProperties({
215
pullRequestUrl: 'https://github.com/owner/repo/pull/42',
216
pullRequestState: 'open',
217
})
218
);
219
gitService.setRepo(createGitRepo());
220
octoKitService.findPullRequestByHeadBranch.mockResolvedValue(
221
createPrSearchItem({ state: 'CLOSED' })
222
);
223
224
service.detectPullRequest('session-1');
225
await vi.waitFor(() => expect(worktreeService.setWorktreeProperties).toHaveBeenCalledWith(
226
'session-1',
227
expect.objectContaining({ pullRequestState: 'closed' }),
228
));
229
});
230
231
it('does not update properties when no PR is found via GitHub API', async () => {
232
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
233
gitService.setRepo(createGitRepo());
234
octoKitService.findPullRequestByHeadBranch.mockResolvedValue(undefined);
235
236
service.detectPullRequest('session-1');
237
await vi.waitFor(() => expect(octoKitService.findPullRequestByHeadBranch).toHaveBeenCalled());
238
expect(worktreeService.setWorktreeProperties).not.toHaveBeenCalled();
239
});
240
241
it('does not throw on error', async () => {
242
worktreeService.getWorktreeProperties.mockRejectedValue(new Error('Service down'));
243
service.detectPullRequest('session-1');
244
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
245
});
246
247
it('does not query GitHub API when git repository is not found', async () => {
248
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
249
gitService.getRepository.mockResolvedValue(undefined);
250
251
service.detectPullRequest('session-1');
252
await vi.waitFor(() => expect(gitService.getRepository).toHaveBeenCalled());
253
expect(octoKitService.findPullRequestByHeadBranch).not.toHaveBeenCalled();
254
});
255
});
256
257
describe('handlePullRequestCreated', () => {
258
it('does not persist when no worktree properties exist', async () => {
259
worktreeService.getWorktreeProperties.mockResolvedValue(undefined);
260
service.handlePullRequestCreated('session-1', 'https://github.com/owner/repo/pull/42');
261
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
262
expect(worktreeService.setWorktreeProperties).not.toHaveBeenCalled();
263
});
264
265
it('does not persist when version is not 2', async () => {
266
worktreeService.getWorktreeProperties.mockResolvedValue({
267
version: 1,
268
autoCommit: true,
269
baseCommit: 'abc',
270
branchName: 'branch',
271
repositoryPath: '/repo',
272
worktreePath: '/wt',
273
});
274
service.handlePullRequestCreated('session-1', 'https://github.com/owner/repo/pull/42');
275
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
276
expect(worktreeService.setWorktreeProperties).not.toHaveBeenCalled();
277
});
278
279
it('persists PR URL from session when provided', async () => {
280
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
281
service.handlePullRequestCreated('session-1', 'https://github.com/owner/repo/pull/99');
282
await vi.waitFor(() => expect(worktreeService.setWorktreeProperties).toHaveBeenCalledWith(
283
'session-1',
284
expect.objectContaining({
285
pullRequestUrl: 'https://github.com/owner/repo/pull/99',
286
}),
287
));
288
});
289
290
it('fires onDidDetectPullRequest when PR is persisted', async () => {
291
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
292
const firedSessionIds: string[] = [];
293
service.onDidDetectPullRequest(id => firedSessionIds.push(id));
294
295
service.handlePullRequestCreated('session-1', 'https://github.com/owner/repo/pull/99');
296
await vi.waitFor(() => expect(firedSessionIds).toEqual(['session-1']));
297
});
298
299
it('does not fire onDidDetectPullRequest when no PR detected', async () => {
300
worktreeService.getWorktreeProperties.mockResolvedValue(
301
createV2WorktreeProperties({ branchName: '', repositoryPath: '' })
302
);
303
const firedSessionIds: string[] = [];
304
service.onDidDetectPullRequest(id => firedSessionIds.push(id));
305
306
service.handlePullRequestCreated('session-1', undefined);
307
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
308
expect(firedSessionIds).toEqual([]);
309
});
310
311
it('does not persist when no PR URL and no branch/repo for retry', async () => {
312
worktreeService.getWorktreeProperties.mockResolvedValue(
313
createV2WorktreeProperties({ branchName: '', repositoryPath: '' })
314
);
315
service.handlePullRequestCreated('session-1', undefined);
316
await vi.waitFor(() => expect(worktreeService.getWorktreeProperties).toHaveBeenCalled());
317
expect(worktreeService.setWorktreeProperties).not.toHaveBeenCalled();
318
});
319
320
it('does not fire event when setWorktreeProperties throws', async () => {
321
worktreeService.getWorktreeProperties.mockResolvedValue(createV2WorktreeProperties());
322
worktreeService.setWorktreeProperties.mockRejectedValue(new Error('Write failed'));
323
const firedSessionIds: string[] = [];
324
service.onDidDetectPullRequest(id => firedSessionIds.push(id));
325
326
service.handlePullRequestCreated('session-1', 'https://github.com/owner/repo/pull/42');
327
await vi.waitFor(() => expect(worktreeService.setWorktreeProperties).toHaveBeenCalled());
328
expect(firedSessionIds).toEqual([]);
329
});
330
});
331
});
332
333