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/folderRepositoryManager.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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
import * as vscode from 'vscode';
8
import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService';
9
import { IGitService, RepoContext } from '../../../../platform/git/common/gitService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { NullWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
12
import { mock } from '../../../../util/common/test/simpleMock';
13
import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';
14
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
15
import { URI } from '../../../../util/vs/base/common/uri';
16
import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../vscodeTypes';
17
import { MockChatResponseStream } from '../../../test/node/testHelpers';
18
import type { IToolsService } from '../../../tools/common/toolsService';
19
import { RepositoryProperties } from '../../common/chatSessionMetadataStore';
20
import { IChatSessionWorkspaceFolderService } from '../../common/chatSessionWorkspaceFolderService';
21
import { ChatSessionWorktreeFile, ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../../common/chatSessionWorktreeService';
22
import { IFolderRepositoryManager } from '../../common/folderRepositoryManager';
23
import { ICopilotCLISessionService } from '../../copilotcli/node/copilotcliSessionService';
24
import { ClaudeFolderRepositoryManager, CopilotCLIFolderRepositoryManager } from '../folderRepositoryManagerImpl';
25
import { MockChatSessionMetadataStore } from '../../common/test/mockChatSessionMetadataStore';
26
import type { IClaudeSessionStateService } from '../../claude/common/claudeSessionStateService';
27
import type { ClaudeFolderInfo } from '../../claude/common/claudeFolderInfo';
28
29
/**
30
* Fake implementation of IChatSessionWorktreeService for testing.
31
*/
32
class FakeChatSessionWorktreeService extends mock<IChatSessionWorktreeService>() {
33
private _worktreeProperties = new Map<string, ChatSessionWorktreeProperties>();
34
35
override createWorktree = vi.fn(async (_repositoryPath: vscode.Uri, _stream?: vscode.ChatResponseStream, _baseBranch?: string): Promise<ChatSessionWorktreeProperties | undefined> => {
36
return undefined;
37
});
38
39
override getWorktreeProperties = vi.fn(async (sessionId: string | vscode.Uri): Promise<ChatSessionWorktreeProperties | undefined> => {
40
return this._worktreeProperties.get(typeof sessionId === 'string' ? sessionId : sessionId.fsPath);
41
});
42
43
override setWorktreeProperties = vi.fn(async (sessionId: string, properties: string | ChatSessionWorktreeProperties): Promise<void> => {
44
if (typeof properties === 'string') {
45
return;
46
}
47
this._worktreeProperties.set(sessionId, properties);
48
});
49
50
override getWorktreePath = vi.fn(async (sessionId: string): Promise<vscode.Uri | undefined> => {
51
const props = this._worktreeProperties.get(sessionId);
52
return props ? vscode.Uri.file(props.worktreePath) : undefined;
53
});
54
55
setTestWorktreeProperties(sessionId: string, properties: ChatSessionWorktreeProperties): void {
56
this._worktreeProperties.set(sessionId, properties);
57
}
58
}
59
60
/**
61
* Fake implementation of IChatSessionWorkspaceFolderService for testing.
62
*/
63
class FakeChatSessionWorkspaceFolderService extends mock<IChatSessionWorkspaceFolderService>() {
64
private _sessionWorkspaceFolders = new Map<string, vscode.Uri>();
65
private _sessionWorkspaceFolderRepositories = new Map<string, vscode.Uri | undefined>();
66
private _workspaceChanges = new Map<string, readonly ChatSessionWorktreeFile[] | undefined>();
67
68
override trackSessionWorkspaceFolder = vi.fn(async (sessionId: string, workspaceFolderUri: string, repositoryProperties?: RepositoryProperties): Promise<void> => {
69
this._sessionWorkspaceFolders.set(sessionId, vscode.Uri.file(workspaceFolderUri));
70
this._sessionWorkspaceFolderRepositories.set(sessionId, repositoryProperties?.repositoryPath ? vscode.Uri.file(repositoryProperties.repositoryPath) : undefined);
71
});
72
73
override deleteTrackedWorkspaceFolder = vi.fn(async (sessionId: string): Promise<void> => {
74
this._sessionWorkspaceFolders.delete(sessionId);
75
this._sessionWorkspaceFolderRepositories.delete(sessionId);
76
});
77
78
override getSessionWorkspaceFolder = vi.fn(async (sessionId: string): Promise<vscode.Uri | undefined> => {
79
return this._sessionWorkspaceFolders.get(sessionId);
80
});
81
82
override getSessionWorkspaceFolderEntry = vi.fn(async (sessionId: string) => {
83
const folder = this._sessionWorkspaceFolders.get(sessionId);
84
if (!folder) {
85
return undefined;
86
}
87
88
return {
89
folderPath: folder.fsPath,
90
timestamp: Date.now()
91
};
92
});
93
94
override getRepositoryProperties = vi.fn(async (_sessionId: string): Promise<RepositoryProperties | undefined> => {
95
return undefined;
96
});
97
98
override handleRequestCompleted = vi.fn(async (_sessionId: string): Promise<void> => { });
99
100
override getWorkspaceChanges = vi.fn(async (sessionId: string): Promise<readonly ChatSessionWorktreeFile[] | undefined> => {
101
return this._workspaceChanges.get(sessionId);
102
});
103
104
setTestSessionWorkspaceFolder(sessionId: string, folder: vscode.Uri): void {
105
this._sessionWorkspaceFolders.set(sessionId, folder);
106
}
107
108
override clearWorkspaceChanges(sessionIdOrFolderUri: string | vscode.Uri): string[] {
109
if (typeof sessionIdOrFolderUri === 'string') {
110
this._workspaceChanges.delete(sessionIdOrFolderUri);
111
}
112
return [];
113
}
114
}
115
116
/**
117
* Fake implementation of ICopilotCLISessionService for testing.
118
*/
119
class FakeCopilotCLISessionService extends mock<ICopilotCLISessionService>() {
120
private _sessionWorkingDirs = new Map<string, vscode.Uri>();
121
122
override getSessionWorkingDirectory = vi.fn((sessionId: string): vscode.Uri | undefined => {
123
return this._sessionWorkingDirs.get(sessionId);
124
});
125
126
setTestSessionWorkingDirectory(sessionId: string, uri: vscode.Uri): void {
127
this._sessionWorkingDirs.set(sessionId, uri);
128
}
129
}
130
131
/**
132
* Fake implementation of IGitService for testing.
133
*/
134
class FakeGitService extends mock<IGitService>() {
135
private _repositories = new Map<string, RepoContext>();
136
private _recentRepositories: { rootUri: vscode.Uri; lastAccessTime: number }[] = [];
137
private _activeRepo: RepoContext | undefined;
138
139
override activeRepository = {
140
get: () => this._activeRepo
141
} as unknown as IGitService['activeRepository'];
142
143
override repositories: RepoContext[] = [];
144
145
override async getRepository(uri: vscode.Uri, _forceOpen?: boolean): Promise<RepoContext | undefined> {
146
return this._repositories.get(uri.fsPath);
147
}
148
149
override getRecentRepositories = vi.fn((): { rootUri: vscode.Uri; lastAccessTime: number }[] => {
150
return this._recentRepositories;
151
});
152
153
setTestRepository(uri: vscode.Uri, repo: RepoContext): void {
154
this._repositories.set(uri.fsPath, repo);
155
}
156
157
setTestRecentRepositories(repos: { rootUri: vscode.Uri; lastAccessTime: number }[]): void {
158
this._recentRepositories = repos;
159
}
160
161
setTestActiveRepository(repo: RepoContext | undefined): void {
162
this._activeRepo = repo;
163
if (repo) {
164
this._repositories.set(repo.rootUri.fsPath, repo);
165
}
166
}
167
}
168
169
/**
170
* Mock workspace service that tracks trust requests.
171
*/
172
/**
173
* Fake implementation of IToolsService for testing.
174
*/
175
class FakeToolsService extends mock<IToolsService>() {
176
nextConfirmationButton: string | undefined = undefined;
177
override getTool(name: string) {
178
if (name === 'vscode_get_modified_files_confirmation') {
179
return { name } as any;
180
}
181
return undefined;
182
}
183
override invokeTool = vi.fn(async (name: string, _options: unknown, _token: unknown) => {
184
if (name === 'vscode_get_modified_files_confirmation') {
185
const button = this.nextConfirmationButton;
186
if (button !== undefined) {
187
return new LanguageModelToolResult2([new LanguageModelTextPart(button)]);
188
}
189
return new LanguageModelToolResult2([]);
190
}
191
return new LanguageModelToolResult2([]);
192
});
193
}
194
195
/**
196
* Mock workspace service that tracks trust requests.
197
*/
198
class MockWorkspaceService extends NullWorkspaceService {
199
public trustRequests: vscode.Uri[] = [];
200
public trustResponse = true;
201
202
constructor(folders: vscode.Uri[] = []) {
203
super(folders);
204
}
205
206
override async requestResourceTrust(options: { uri: vscode.Uri; message: string }): Promise<boolean> {
207
this.trustRequests.push(options.uri);
208
return this.trustResponse;
209
}
210
}
211
212
/**
213
* FakeFolderRepositoryManager for use in other tests.
214
* Provides a configurable mock of IFolderRepositoryManager.
215
*/
216
export class FakeFolderRepositoryManager extends mock<IFolderRepositoryManager>() {
217
private _untitledSessionFolders = new Map<string, vscode.Uri>();
218
private _folderRepoInfo = new Map<string, {
219
folder: vscode.Uri | undefined;
220
repository: vscode.Uri | undefined;
221
repositoryProperties?: RepositoryProperties;
222
worktree: vscode.Uri | undefined;
223
trusted: boolean | undefined;
224
worktreeProperties: ChatSessionWorktreeProperties | undefined;
225
}>();
226
227
override setNewSessionFolder = vi.fn((sessionId: string, folderUri: vscode.Uri): void => {
228
if (!sessionId.startsWith('untitled:') && !sessionId.startsWith('untitled-')) {
229
throw new Error(`Cannot set folder for non-untitled session: ${sessionId}`);
230
}
231
this._untitledSessionFolders.set(sessionId, folderUri);
232
});
233
234
override getFolderRepository = vi.fn(async (
235
sessionId: string,
236
_options: { promptForTrust: true; stream: vscode.ChatResponseStream } | undefined,
237
_token: vscode.CancellationToken
238
) => {
239
const info = this._folderRepoInfo.get(sessionId);
240
return info ?? { folder: undefined, repository: undefined, repositoryProperties: undefined, worktree: undefined, trusted: undefined, worktreeProperties: undefined };
241
});
242
243
override initializeFolderRepository = vi.fn(async (
244
sessionId: string | undefined,
245
_options: { stream: vscode.ChatResponseStream; toolInvocationToken: vscode.ChatParticipantToolToken },
246
_token: vscode.CancellationToken
247
) => {
248
const info = sessionId ? this._folderRepoInfo.get(sessionId) : undefined;
249
return {
250
folder: info?.folder,
251
repository: info?.repository,
252
repositoryProperties: info?.repositoryProperties,
253
worktree: info?.worktree,
254
trusted: info?.trusted ?? true,
255
worktreeProperties: info?.worktreeProperties
256
};
257
});
258
259
override getFolderMRU = vi.fn(() => {
260
return Promise.resolve([]);
261
});
262
263
override deleteNewSessionFolder = vi.fn((sessionId: string): void => {
264
this._untitledSessionFolders.delete(sessionId);
265
});
266
267
override getRepositoryInfo = vi.fn(async (
268
_folder: vscode.Uri,
269
_token: vscode.CancellationToken
270
) => {
271
return { repository: undefined, headBranchName: undefined };
272
});
273
274
setTestFolderRepositoryInfo(sessionId: string, info: {
275
folder: vscode.Uri | undefined;
276
repository: vscode.Uri | undefined;
277
repositoryProperties?: RepositoryProperties;
278
worktree: vscode.Uri | undefined;
279
trusted: boolean | undefined;
280
worktreeProperties: ChatSessionWorktreeProperties | undefined;
281
}): void {
282
this._folderRepoInfo.set(sessionId, info);
283
}
284
}
285
286
describe('CopilotCLIFolderRepositoryManager', () => {
287
const disposables = new DisposableStore();
288
let manager: CopilotCLIFolderRepositoryManager;
289
let worktreeService: FakeChatSessionWorktreeService;
290
let workspaceFolderService: FakeChatSessionWorkspaceFolderService;
291
let sessionService: FakeCopilotCLISessionService;
292
let gitService: FakeGitService;
293
let workspaceService: MockWorkspaceService;
294
let logService: ILogService;
295
let toolsService: FakeToolsService;
296
let fileSystem: MockFileSystemService;
297
298
beforeEach(() => {
299
worktreeService = new FakeChatSessionWorktreeService();
300
workspaceFolderService = new FakeChatSessionWorkspaceFolderService();
301
sessionService = new FakeCopilotCLISessionService();
302
gitService = new FakeGitService();
303
workspaceService = new MockWorkspaceService([URI.file('/workspace')]);
304
logService = new class extends mock<ILogService>() {
305
override trace = vi.fn();
306
override info = vi.fn();
307
override warn = vi.fn();
308
override error = vi.fn();
309
}();
310
toolsService = new FakeToolsService();
311
fileSystem = new MockFileSystemService();
312
313
manager = new CopilotCLIFolderRepositoryManager(
314
worktreeService,
315
workspaceFolderService,
316
sessionService,
317
gitService,
318
workspaceService,
319
logService,
320
toolsService,
321
fileSystem,
322
new MockChatSessionMetadataStore()
323
);
324
});
325
326
afterEach(() => {
327
vi.restoreAllMocks();
328
disposables.clear();
329
});
330
331
describe('getFolderRepository', () => {
332
it('returns folder info from memory for untitled sessions', async () => {
333
const sessionId = 'untitled:test-123';
334
const folderUri = vscode.Uri.file('/my/folder');
335
const token = disposables.add(new CancellationTokenSource()).token;
336
337
manager.setNewSessionFolder(sessionId, folderUri);
338
339
const result = await manager.getFolderRepository(sessionId, undefined, token);
340
341
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/my/folder').fsPath);
342
expect(result.repository).toBeUndefined();
343
expect(result.worktree).toBeUndefined();
344
expect(result.trusted).toBeUndefined();
345
});
346
347
it('returns worktree info for sessions with worktrees', async () => {
348
const sessionId = 'cli-123';
349
const token = disposables.add(new CancellationTokenSource()).token;
350
351
worktreeService.setTestWorktreeProperties(sessionId, {
352
autoCommit: true,
353
baseCommit: 'abc123',
354
branchName: 'copilot-worktree',
355
repositoryPath: '/repo',
356
worktreePath: '/repo-worktree',
357
version: 1
358
});
359
360
const result = await manager.getFolderRepository(sessionId, undefined, token);
361
362
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/repo').fsPath);
363
expect(result.repository?.fsPath).toBe(vscode.Uri.file('/repo').fsPath);
364
expect(result.worktree?.fsPath).toBe(vscode.Uri.file('/repo-worktree').fsPath);
365
});
366
367
it('returns workspace folder for sessions without worktrees', async () => {
368
const sessionId = 'cli-456';
369
const token = disposables.add(new CancellationTokenSource()).token;
370
const folderUri = vscode.Uri.file('/workspace/project');
371
372
workspaceFolderService.setTestSessionWorkspaceFolder(sessionId, folderUri);
373
374
const result = await manager.getFolderRepository(sessionId, undefined, token);
375
376
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/workspace/project').fsPath);
377
expect(result.repository).toBeUndefined();
378
expect(result.worktree).toBeUndefined();
379
});
380
381
it('falls back to CLI session working directory', async () => {
382
const sessionId = 'cli-789';
383
const token = disposables.add(new CancellationTokenSource()).token;
384
const cwdUri = vscode.Uri.file('/terminal/cwd');
385
386
sessionService.setTestSessionWorkingDirectory(sessionId, cwdUri);
387
await fileSystem.createDirectory(URI.file('/terminal/cwd'));
388
389
const result = await manager.getFolderRepository(sessionId, undefined, token);
390
391
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/terminal/cwd').fsPath);
392
});
393
394
it('prompts for trust when option is set', async () => {
395
const sessionId = 'cli-123';
396
const token = disposables.add(new CancellationTokenSource()).token;
397
const stream = new MockChatResponseStream();
398
399
worktreeService.setTestWorktreeProperties(sessionId, {
400
autoCommit: true,
401
baseCommit: 'abc123',
402
branchName: 'copilot-worktree',
403
repositoryPath: '/repo',
404
worktreePath: '/repo-worktree',
405
version: 1
406
});
407
408
const result = await manager.getFolderRepository(
409
sessionId,
410
{ promptForTrust: true, stream },
411
token
412
);
413
414
expect(result.trusted).toBe(true);
415
expect(workspaceService.trustRequests.length).toBe(1);
416
});
417
418
it('returns trusted: false when trust denied', async () => {
419
const sessionId = 'cli-123';
420
const token = disposables.add(new CancellationTokenSource()).token;
421
const stream = new MockChatResponseStream();
422
workspaceService.trustResponse = false;
423
424
worktreeService.setTestWorktreeProperties(sessionId, {
425
autoCommit: true,
426
baseCommit: 'abc123',
427
branchName: 'copilot-worktree',
428
repositoryPath: '/repo',
429
worktreePath: '/repo-worktree',
430
version: 1
431
});
432
433
const result = await manager.getFolderRepository(
434
sessionId,
435
{ promptForTrust: true, stream },
436
token
437
);
438
439
expect(result.trusted).toBe(false);
440
});
441
442
it('checks trust on repository path, not worktree path', async () => {
443
const sessionId = 'cli-123';
444
const token = disposables.add(new CancellationTokenSource()).token;
445
const stream = new MockChatResponseStream();
446
447
worktreeService.setTestWorktreeProperties(sessionId, {
448
autoCommit: true,
449
baseCommit: 'abc123',
450
branchName: 'copilot-worktree',
451
repositoryPath: '/original-repo',
452
worktreePath: '/worktree-path',
453
version: 1
454
});
455
456
await manager.getFolderRepository(
457
sessionId,
458
{ promptForTrust: true, stream },
459
token
460
);
461
462
// Trust should be checked on repository path, not worktree path
463
expect(workspaceService.trustRequests[0].fsPath).toBe(vscode.Uri.file('/original-repo').fsPath);
464
});
465
});
466
467
describe('initializeFolderRepository', () => {
468
const mockToolInvocationToken = {} as vscode.ChatParticipantToolToken;
469
470
it('creates worktree when git repo selected', async () => {
471
const sessionId = 'untitled:test-123';
472
const token = disposables.add(new CancellationTokenSource()).token;
473
const stream = new MockChatResponseStream();
474
const folderUri = vscode.Uri.file('/my/repo');
475
476
manager.setNewSessionFolder(sessionId, folderUri);
477
gitService.setTestRepository(folderUri, {
478
rootUri: folderUri,
479
remotes: [] as string[],
480
kind: 'repository'
481
} as RepoContext);
482
483
(worktreeService.createWorktree as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
484
autoCommit: true,
485
baseCommit: 'abc123',
486
branchName: 'copilot-worktree',
487
repositoryPath: '/my/repo',
488
worktreePath: '/my/repo-worktree',
489
version: 1
490
} satisfies ChatSessionWorktreeProperties);
491
492
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
493
494
expect(result.worktree?.fsPath).toBe(vscode.Uri.file('/my/repo-worktree').fsPath);
495
expect(result.repository?.fsPath).toBe(vscode.Uri.file('/my/repo').fsPath);
496
expect(result.trusted).toBe(true);
497
});
498
499
it('falls back to folder when worktree creation fails', async () => {
500
const sessionId = 'untitled:test-123';
501
const token = disposables.add(new CancellationTokenSource()).token;
502
const stream = new MockChatResponseStream();
503
const folderUri = vscode.Uri.file('/my/repo');
504
505
manager.setNewSessionFolder(sessionId, folderUri);
506
gitService.setTestRepository(folderUri, {
507
rootUri: folderUri,
508
remotes: [] as string[],
509
kind: 'repository'
510
} as RepoContext);
511
512
(worktreeService.createWorktree as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
513
514
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
515
516
expect(result.worktree).toBeUndefined();
517
expect(result.repository?.fsPath).toBe(vscode.Uri.file('/my/repo').fsPath);
518
expect(stream.output.some(o => /failed to create worktree/i.test(o))).toBe(true);
519
});
520
521
it('handles workspace folder without git repo', async () => {
522
const sessionId = 'untitled:test-123';
523
const token = disposables.add(new CancellationTokenSource()).token;
524
const stream = new MockChatResponseStream();
525
const folderUri = vscode.Uri.file('/plain/folder');
526
527
manager.setNewSessionFolder(sessionId, folderUri);
528
// No git repo set for this folder
529
530
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
531
532
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/plain/folder').fsPath);
533
expect(result.repository).toBeUndefined();
534
expect(result.worktree).toBeUndefined();
535
expect(result.trusted).toBe(true);
536
});
537
538
it('returns trusted: false when trust denied', async () => {
539
const sessionId = 'untitled:test-123';
540
const token = disposables.add(new CancellationTokenSource()).token;
541
const stream = new MockChatResponseStream();
542
const folderUri = vscode.Uri.file('/my/repo');
543
workspaceService.trustResponse = false;
544
545
// Use empty workspace to trigger trust check
546
workspaceService = new MockWorkspaceService([]);
547
workspaceService.trustResponse = false;
548
manager = new CopilotCLIFolderRepositoryManager(
549
worktreeService,
550
workspaceFolderService,
551
sessionService,
552
gitService,
553
workspaceService,
554
logService,
555
toolsService,
556
new MockFileSystemService(),
557
new MockChatSessionMetadataStore()
558
);
559
560
manager.setNewSessionFolder(sessionId, folderUri);
561
562
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
563
564
expect(result.trusted).toBe(false);
565
});
566
});
567
568
describe('uncommitted changes prompting in initializeFolderRepository', () => {
569
const mockToolInvocationToken = {} as vscode.ChatParticipantToolToken;
570
571
it('prompts when untitled session has repo with index changes', async () => {
572
const sessionId = 'untitled:test-123';
573
const folderUri = vscode.Uri.file('/my/repo');
574
const token = disposables.add(new CancellationTokenSource()).token;
575
const stream = new MockChatResponseStream();
576
toolsService.nextConfirmationButton = 'Copy Changes';
577
578
manager.setNewSessionFolder(sessionId, folderUri);
579
gitService.setTestRepository(folderUri, {
580
rootUri: folderUri,
581
kind: 'repository',
582
remotes: [] as string[],
583
changes: { indexChanges: [{ path: 'file.ts' }], workingTree: [], mergeChanges: [], untrackedChanges: [] }
584
} as unknown as RepoContext);
585
586
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
587
expect(toolsService.invokeTool).toHaveBeenCalledWith(
588
'vscode_get_modified_files_confirmation',
589
expect.objectContaining({
590
input: expect.objectContaining({
591
title: 'Uncommitted Changes',
592
options: ['Copy Changes', 'Move Changes', 'Skip Changes'],
593
modifiedFiles: [
594
expect.objectContaining({
595
uri: expect.objectContaining({ path: '/my/repo/file.ts', scheme: 'file' })
596
})
597
]
598
})
599
}),
600
token
601
);
602
});
603
604
it('prompts when untitled session has repo with working tree changes', async () => {
605
const sessionId = 'untitled:test-123';
606
const folderUri = vscode.Uri.file('/my/repo');
607
const token = disposables.add(new CancellationTokenSource()).token;
608
const stream = new MockChatResponseStream();
609
toolsService.nextConfirmationButton = 'Copy Changes';
610
611
manager.setNewSessionFolder(sessionId, folderUri);
612
gitService.setTestRepository(folderUri, {
613
rootUri: folderUri,
614
kind: 'repository',
615
remotes: [] as string[],
616
changes: { indexChanges: [], workingTree: [{ path: 'file.ts' }], mergeChanges: [], untrackedChanges: [] }
617
} as unknown as RepoContext);
618
619
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
620
expect(toolsService.invokeTool).toHaveBeenCalled();
621
});
622
623
it('does not prompt when untitled session has repo with no changes', async () => {
624
const sessionId = 'untitled:test-123';
625
const folderUri = vscode.Uri.file('/my/repo');
626
const token = disposables.add(new CancellationTokenSource()).token;
627
const stream = new MockChatResponseStream();
628
629
manager.setNewSessionFolder(sessionId, folderUri);
630
gitService.setTestRepository(folderUri, {
631
rootUri: folderUri,
632
kind: 'repository',
633
remotes: [] as string[],
634
changes: { indexChanges: [], workingTree: [], mergeChanges: [], untrackedChanges: [] }
635
} as unknown as RepoContext);
636
637
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
638
expect(toolsService.invokeTool).not.toHaveBeenCalled();
639
});
640
641
it('does not prompt when untitled session folder has no git repo', async () => {
642
const sessionId = 'untitled:test-123';
643
const folderUri = vscode.Uri.file('/plain/folder');
644
const token = disposables.add(new CancellationTokenSource()).token;
645
const stream = new MockChatResponseStream();
646
647
manager.setNewSessionFolder(sessionId, folderUri);
648
649
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
650
expect(toolsService.invokeTool).not.toHaveBeenCalled();
651
});
652
653
it('returns cancelled when user cancels', async () => {
654
const sessionId = 'untitled:test-123';
655
const folderUri = vscode.Uri.file('/my/repo');
656
const token = disposables.add(new CancellationTokenSource()).token;
657
const stream = new MockChatResponseStream();
658
toolsService.nextConfirmationButton = 'Cancel';
659
660
manager.setNewSessionFolder(sessionId, folderUri);
661
gitService.setTestRepository(folderUri, {
662
rootUri: folderUri,
663
kind: 'repository',
664
remotes: [] as string[],
665
changes: { indexChanges: [{ path: 'file.ts' }], workingTree: [], mergeChanges: [], untrackedChanges: [] }
666
} as unknown as RepoContext);
667
668
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
669
expect(result.cancelled).toBe(true);
670
});
671
672
it('uses delegation title when no session ID', async () => {
673
const token = disposables.add(new CancellationTokenSource()).token;
674
const stream = new MockChatResponseStream();
675
toolsService.nextConfirmationButton = 'Copy Changes';
676
677
gitService.setTestActiveRepository({
678
rootUri: vscode.Uri.file('/workspace'),
679
remotes: [] as string[],
680
kind: 'repository',
681
changes: { indexChanges: [{ path: 'file.ts' }], workingTree: [], mergeChanges: [], untrackedChanges: [] }
682
} as unknown as RepoContext);
683
684
await manager.initializeFolderRepository(undefined, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
685
expect(toolsService.invokeTool).toHaveBeenCalledWith(
686
'vscode_get_modified_files_confirmation',
687
expect.objectContaining({
688
input: expect.objectContaining({
689
title: 'Delegate to Copilot CLI',
690
modifiedFiles: [
691
expect.objectContaining({
692
uri: expect.objectContaining({ path: '/workspace/file.ts', scheme: 'file' })
693
})
694
]
695
})
696
}),
697
token
698
);
699
});
700
701
it('does not prompt for delegation without active repository', async () => {
702
const token = disposables.add(new CancellationTokenSource()).token;
703
const stream = new MockChatResponseStream();
704
705
await manager.initializeFolderRepository(undefined, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
706
expect(toolsService.invokeTool).not.toHaveBeenCalled();
707
});
708
709
it('does not prompt for delegation in welcome view (no workspace folders)', async () => {
710
workspaceService = new MockWorkspaceService([]);
711
manager = new CopilotCLIFolderRepositoryManager(
712
worktreeService,
713
workspaceFolderService,
714
sessionService,
715
gitService,
716
workspaceService,
717
logService,
718
toolsService,
719
new MockFileSystemService(),
720
new MockChatSessionMetadataStore()
721
);
722
const token = disposables.add(new CancellationTokenSource()).token;
723
const stream = new MockChatResponseStream();
724
toolsService.nextConfirmationButton = 'Copy Changes';
725
726
gitService.setTestActiveRepository({
727
rootUri: vscode.Uri.file('/workspace'),
728
remotes: [] as string[],
729
kind: 'repository',
730
changes: { indexChanges: [{ path: 'file.ts' }], workingTree: [], mergeChanges: [], untrackedChanges: [] }
731
} as unknown as RepoContext);
732
733
await manager.initializeFolderRepository(undefined, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
734
expect(toolsService.invokeTool).not.toHaveBeenCalled();
735
});
736
});
737
738
describe('worktree folder opened as workspace folder', () => {
739
const mockToolInvocationToken = {} as vscode.ChatParticipantToolToken;
740
const worktreeFolderPath = vscode.Uri.file('/repo-worktree').fsPath;
741
const originalRepoPath = vscode.Uri.file('/original-repo').fsPath;
742
const defaultWorktreeProps: ChatSessionWorktreeProperties = {
743
autoCommit: true,
744
baseCommit: 'abc123',
745
branchName: 'copilot-worktree',
746
repositoryPath: originalRepoPath,
747
worktreePath: worktreeFolderPath,
748
version: 1
749
};
750
751
describe('initializeFolderRepository', () => {
752
function createMetadataStoreWithWorktree(): MockChatSessionMetadataStore {
753
const store = new MockChatSessionMetadataStore();
754
// Register a session whose worktree path matches worktreeFolderPath so that
755
// getWorktreeSessions(folderUri) returns a session ID that the worktreeService
756
// can resolve via getWorktreeProperties.
757
void store.storeWorktreeInfo(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
758
return store;
759
}
760
761
it('skips worktree creation when single workspace folder is already a tracked worktree', async () => {
762
workspaceService = new MockWorkspaceService([URI.file(worktreeFolderPath)]);
763
gitService.setTestActiveRepository({
764
rootUri: vscode.Uri.file(worktreeFolderPath),
765
kind: 'repository'
766
} as RepoContext);
767
worktreeService.setTestWorktreeProperties(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
768
manager = new CopilotCLIFolderRepositoryManager(
769
worktreeService, workspaceFolderService, sessionService,
770
gitService, workspaceService, logService, toolsService,
771
new MockFileSystemService(),
772
createMetadataStoreWithWorktree()
773
);
774
775
const sessionId = 'untitled:wt-test-1';
776
const token = disposables.add(new CancellationTokenSource()).token;
777
const stream = new MockChatResponseStream();
778
779
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
780
781
expect(worktreeService.createWorktree).not.toHaveBeenCalled();
782
expect(result.worktreeProperties).toBeDefined();
783
expect(result.worktree?.fsPath).toBe(vscode.Uri.file(worktreeFolderPath).fsPath);
784
expect(result.repository?.fsPath).toBe(vscode.Uri.file(originalRepoPath).fsPath);
785
expect(result.trusted).toBe(true);
786
});
787
788
it('skips worktree creation when explicitly selected folder is a tracked worktree', async () => {
789
worktreeService.setTestWorktreeProperties(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
790
manager = new CopilotCLIFolderRepositoryManager(
791
worktreeService, workspaceFolderService, sessionService,
792
gitService, workspaceService, logService, toolsService,
793
new MockFileSystemService(),
794
createMetadataStoreWithWorktree()
795
);
796
797
const sessionId = 'untitled:wt-test-2';
798
const token = disposables.add(new CancellationTokenSource()).token;
799
const stream = new MockChatResponseStream();
800
801
manager.setNewSessionFolder(sessionId, vscode.Uri.file(worktreeFolderPath));
802
803
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
804
805
expect(worktreeService.createWorktree).not.toHaveBeenCalled();
806
expect(result.worktreeProperties).toBeDefined();
807
expect(result.worktree?.fsPath).toBe(vscode.Uri.file(worktreeFolderPath).fsPath);
808
expect(result.repository?.fsPath).toBe(vscode.Uri.file(originalRepoPath).fsPath);
809
expect(result.trusted).toBe(true);
810
});
811
812
it('skips uncommitted changes prompt when worktree already detected via single workspace folder', async () => {
813
workspaceService = new MockWorkspaceService([URI.file(worktreeFolderPath)]);
814
gitService.setTestActiveRepository({
815
rootUri: vscode.Uri.file(worktreeFolderPath),
816
kind: 'repository',
817
remotes: [] as string[],
818
changes: { indexChanges: [{ path: 'file.ts' }], workingTree: [{ path: 'other.ts' }], mergeChanges: [], untrackedChanges: [] }
819
} as unknown as RepoContext);
820
worktreeService.setTestWorktreeProperties(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
821
manager = new CopilotCLIFolderRepositoryManager(
822
worktreeService, workspaceFolderService, sessionService,
823
gitService, workspaceService, logService, toolsService,
824
new MockFileSystemService(),
825
createMetadataStoreWithWorktree()
826
);
827
828
const sessionId = 'untitled:wt-test-3';
829
const token = disposables.add(new CancellationTokenSource()).token;
830
const stream = new MockChatResponseStream();
831
832
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
833
834
expect(toolsService.invokeTool).not.toHaveBeenCalled();
835
expect(worktreeService.createWorktree).not.toHaveBeenCalled();
836
});
837
838
it('skips uncommitted changes prompt when worktree already detected via explicit selection', async () => {
839
worktreeService.setTestWorktreeProperties(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
840
gitService.setTestRepository(vscode.Uri.file(worktreeFolderPath), {
841
rootUri: vscode.Uri.file(worktreeFolderPath),
842
kind: 'repository',
843
remotes: [] as string[],
844
changes: { indexChanges: [{ path: 'file.ts' }], workingTree: [{ path: 'other.ts' }], mergeChanges: [], untrackedChanges: [] }
845
} as unknown as RepoContext);
846
manager = new CopilotCLIFolderRepositoryManager(
847
worktreeService, workspaceFolderService, sessionService,
848
gitService, workspaceService, logService, toolsService,
849
new MockFileSystemService(),
850
createMetadataStoreWithWorktree()
851
);
852
853
const sessionId = 'untitled:wt-test-4';
854
const token = disposables.add(new CancellationTokenSource()).token;
855
const stream = new MockChatResponseStream();
856
857
manager.setNewSessionFolder(sessionId, vscode.Uri.file(worktreeFolderPath));
858
859
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
860
861
expect(toolsService.invokeTool).not.toHaveBeenCalled();
862
expect(worktreeService.createWorktree).not.toHaveBeenCalled();
863
});
864
865
it('resolves repository path from worktree properties instead of git service', async () => {
866
const differentRepo = '/different-repo';
867
worktreeService.setTestWorktreeProperties(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
868
// Git service would return a different repo for this folder
869
gitService.setTestRepository(vscode.Uri.file(worktreeFolderPath), {
870
rootUri: vscode.Uri.file(differentRepo),
871
kind: 'repository',
872
remotes: [] as string[],
873
} as RepoContext);
874
manager = new CopilotCLIFolderRepositoryManager(
875
worktreeService, workspaceFolderService, sessionService,
876
gitService, workspaceService, logService, toolsService,
877
new MockFileSystemService(),
878
createMetadataStoreWithWorktree()
879
);
880
881
const sessionId = 'untitled:wt-test-5';
882
const token = disposables.add(new CancellationTokenSource()).token;
883
const stream = new MockChatResponseStream();
884
885
manager.setNewSessionFolder(sessionId, vscode.Uri.file(worktreeFolderPath));
886
887
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
888
889
// Should use repositoryPath from worktreeProperties, not from git service
890
expect(result.repository?.fsPath).toBe(vscode.Uri.file(originalRepoPath).fsPath);
891
});
892
893
it('verifies trust on original repository path from worktree properties', async () => {
894
workspaceService = new MockWorkspaceService([URI.file(worktreeFolderPath)]);
895
workspaceService.trustResponse = false;
896
gitService.setTestActiveRepository({
897
rootUri: vscode.Uri.file(worktreeFolderPath),
898
remotes: [] as string[],
899
kind: 'repository'
900
} as RepoContext);
901
worktreeService.setTestWorktreeProperties(vscode.Uri.file(worktreeFolderPath).fsPath, defaultWorktreeProps);
902
manager = new CopilotCLIFolderRepositoryManager(
903
worktreeService, workspaceFolderService, sessionService,
904
gitService, workspaceService, logService, toolsService,
905
new MockFileSystemService(),
906
createMetadataStoreWithWorktree()
907
);
908
909
const sessionId = 'untitled:wt-test-6';
910
const token = disposables.add(new CancellationTokenSource()).token;
911
const stream = new MockChatResponseStream();
912
913
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
914
915
// Trust should be checked on the original repository path, not the worktree folder
916
expect(workspaceService.trustRequests.some(uri => uri.fsPath === vscode.Uri.file(originalRepoPath).fsPath)).toBe(true);
917
expect(result.trusted).toBe(false);
918
});
919
920
it('still creates worktree when folder is not a tracked worktree', async () => {
921
const regularRepo = vscode.Uri.file('/regular-repo');
922
workspaceService = new MockWorkspaceService([URI.file('/regular-repo')]);
923
gitService.setTestActiveRepository({
924
rootUri: regularRepo,
925
remotes: [] as string[],
926
kind: 'repository'
927
} as RepoContext);
928
// NO worktree properties registered — folder is not a tracked worktree
929
manager = new CopilotCLIFolderRepositoryManager(
930
worktreeService, workspaceFolderService, sessionService,
931
gitService, workspaceService, logService, toolsService,
932
new MockFileSystemService(),
933
new MockChatSessionMetadataStore()
934
);
935
936
(worktreeService.createWorktree as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
937
autoCommit: true,
938
baseCommit: 'def456',
939
branchName: 'copilot-new-wt',
940
repositoryPath: '/regular-repo',
941
worktreePath: '/regular-repo-worktree',
942
version: 1
943
} satisfies ChatSessionWorktreeProperties);
944
945
const sessionId = 'untitled:wt-test-7';
946
const token = disposables.add(new CancellationTokenSource()).token;
947
const stream = new MockChatResponseStream();
948
949
const result = await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
950
951
expect(worktreeService.createWorktree).toHaveBeenCalled();
952
expect(result.worktreeProperties).toBeDefined();
953
expect(result.worktree?.fsPath).toBe(vscode.Uri.file('/regular-repo-worktree').fsPath);
954
});
955
});
956
});
957
958
describe('getRepositoryInfo', () => {
959
it('returns repository and head branch for a git repo folder', async () => {
960
const folderUri = vscode.Uri.file('/my/repo');
961
const token = disposables.add(new CancellationTokenSource()).token;
962
963
gitService.setTestRepository(folderUri, {
964
rootUri: folderUri,
965
kind: 'repository',
966
headBranchName: 'main',
967
headCommitHash: 'abc123',
968
remotes: [] as string[]
969
} as RepoContext);
970
971
const result = await manager.getRepositoryInfo(folderUri, token);
972
973
expect(result.repository?.fsPath).toBe(vscode.Uri.file('/my/repo').fsPath);
974
expect(result.headBranchName).toBe('main');
975
});
976
977
it('returns undefined repository for a non-git folder', async () => {
978
const folderUri = vscode.Uri.file('/plain/folder');
979
const token = disposables.add(new CancellationTokenSource()).token;
980
981
const result = await manager.getRepositoryInfo(folderUri, token);
982
983
expect(result.repository).toBeUndefined();
984
expect(result.headBranchName).toBeUndefined();
985
});
986
});
987
988
describe('initializeFolderRepository with branch', () => {
989
const mockToolInvocationToken = {} as vscode.ChatParticipantToolToken;
990
991
it('passes branch to createWorktree when provided', async () => {
992
const sessionId = 'untitled:test-branch';
993
const token = disposables.add(new CancellationTokenSource()).token;
994
const stream = new MockChatResponseStream();
995
const folderUri = vscode.Uri.file('/my/repo');
996
997
manager.setNewSessionFolder(sessionId, folderUri);
998
gitService.setTestRepository(folderUri, {
999
rootUri: folderUri,
1000
kind: 'repository',
1001
remotes: [] as string[]
1002
} as RepoContext);
1003
1004
(worktreeService.createWorktree as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
1005
autoCommit: true,
1006
baseCommit: 'abc123',
1007
branchName: 'copilot-worktree',
1008
repositoryPath: '/my/repo',
1009
worktreePath: '/my/repo-worktree',
1010
version: 1
1011
} satisfies ChatSessionWorktreeProperties);
1012
1013
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, branch: 'feature-branch', folder: undefined }, token);
1014
1015
expect(worktreeService.createWorktree).toHaveBeenCalledWith(
1016
expect.anything(),
1017
expect.anything(),
1018
'feature-branch',
1019
undefined,
1020
);
1021
});
1022
1023
it('passes undefined branch when not provided', async () => {
1024
const sessionId = 'untitled:test-no-branch';
1025
const token = disposables.add(new CancellationTokenSource()).token;
1026
const stream = new MockChatResponseStream();
1027
const folderUri = vscode.Uri.file('/my/repo');
1028
1029
manager.setNewSessionFolder(sessionId, folderUri);
1030
gitService.setTestRepository(folderUri, {
1031
rootUri: folderUri,
1032
kind: 'repository',
1033
remotes: [] as string[]
1034
} as RepoContext);
1035
1036
(worktreeService.createWorktree as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
1037
autoCommit: true,
1038
baseCommit: 'abc123',
1039
branchName: 'copilot-worktree',
1040
repositoryPath: '/my/repo',
1041
worktreePath: '/my/repo-worktree',
1042
version: 1
1043
} satisfies ChatSessionWorktreeProperties);
1044
1045
await manager.initializeFolderRepository(sessionId, { stream, toolInvocationToken: mockToolInvocationToken, folder: undefined }, token);
1046
1047
expect(worktreeService.createWorktree).toHaveBeenCalledWith(
1048
expect.anything(),
1049
expect.anything(),
1050
undefined,
1051
undefined
1052
);
1053
});
1054
});
1055
1056
describe('edge cases', () => {
1057
it('handles empty workspace scenarios', async () => {
1058
// Create manager with no workspace folders
1059
workspaceService = new MockWorkspaceService([]);
1060
manager = new CopilotCLIFolderRepositoryManager(
1061
worktreeService,
1062
workspaceFolderService,
1063
sessionService,
1064
gitService,
1065
workspaceService,
1066
logService,
1067
toolsService,
1068
new MockFileSystemService(),
1069
new MockChatSessionMetadataStore()
1070
);
1071
1072
const sessionId = 'untitled:empty-test';
1073
const folderUri = vscode.Uri.file('/selected/folder');
1074
const token = disposables.add(new CancellationTokenSource()).token;
1075
1076
manager.setNewSessionFolder(sessionId, folderUri);
1077
1078
const result = await manager.getFolderRepository(sessionId, undefined, token);
1079
1080
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/selected/folder').fsPath);
1081
});
1082
1083
it('returns undefined for unknown session', async () => {
1084
const sessionId = 'unknown-session';
1085
const token = disposables.add(new CancellationTokenSource()).token;
1086
1087
const result = await manager.getFolderRepository(sessionId, undefined, token);
1088
1089
expect(result.folder).toBeUndefined();
1090
expect(result.repository).toBeUndefined();
1091
expect(result.worktree).toBeUndefined();
1092
});
1093
});
1094
});
1095
1096
describe('ClaudeFolderRepositoryManager', () => {
1097
const disposables = new DisposableStore();
1098
let manager: ClaudeFolderRepositoryManager;
1099
let worktreeService: FakeChatSessionWorktreeService;
1100
let workspaceFolderService: FakeChatSessionWorkspaceFolderService;
1101
let gitService: FakeGitService;
1102
let workspaceService: MockWorkspaceService;
1103
let logService: ILogService;
1104
let toolsService: FakeToolsService;
1105
let sessionStateService: IClaudeSessionStateService;
1106
let folderInfoMap: Map<string, ClaudeFolderInfo>;
1107
let fileSystem: MockFileSystemService;
1108
1109
beforeEach(() => {
1110
worktreeService = new FakeChatSessionWorktreeService();
1111
workspaceFolderService = new FakeChatSessionWorkspaceFolderService();
1112
gitService = new FakeGitService();
1113
workspaceService = new MockWorkspaceService([URI.file('/workspace')]);
1114
logService = new class extends mock<ILogService>() {
1115
override trace = vi.fn();
1116
override info = vi.fn();
1117
override warn = vi.fn();
1118
override error = vi.fn();
1119
}();
1120
toolsService = new FakeToolsService();
1121
fileSystem = new MockFileSystemService();
1122
1123
folderInfoMap = new Map();
1124
sessionStateService = new class extends mock<IClaudeSessionStateService>() {
1125
override getFolderInfoForSession(sessionId: string): ClaudeFolderInfo | undefined {
1126
return folderInfoMap.get(sessionId);
1127
}
1128
}();
1129
1130
manager = new ClaudeFolderRepositoryManager(
1131
worktreeService,
1132
workspaceFolderService,
1133
gitService,
1134
workspaceService,
1135
logService,
1136
toolsService,
1137
sessionStateService,
1138
fileSystem,
1139
new MockChatSessionMetadataStore()
1140
);
1141
});
1142
1143
afterEach(() => {
1144
vi.restoreAllMocks();
1145
disposables.clear();
1146
});
1147
1148
describe('getFolderRepository', () => {
1149
it('returns worktree info for sessions with worktrees', async () => {
1150
const sessionId = 'test-session';
1151
const token = disposables.add(new CancellationTokenSource()).token;
1152
1153
worktreeService.setTestWorktreeProperties(sessionId, {
1154
autoCommit: true,
1155
baseCommit: 'abc123',
1156
branchName: 'test-branch',
1157
repositoryPath: '/repo/path',
1158
worktreePath: '/worktree/path',
1159
version: 1
1160
});
1161
1162
const result = await manager.getFolderRepository(sessionId, undefined, token);
1163
1164
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/repo/path').fsPath);
1165
expect(result.worktree?.fsPath).toBe(vscode.Uri.file('/worktree/path').fsPath);
1166
});
1167
1168
it('returns workspace folder for sessions without worktrees', async () => {
1169
const sessionId = 'test-session';
1170
const token = disposables.add(new CancellationTokenSource()).token;
1171
1172
workspaceFolderService.setTestSessionWorkspaceFolder(sessionId, vscode.Uri.file('/workspace/folder'));
1173
1174
const result = await manager.getFolderRepository(sessionId, undefined, token);
1175
1176
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/workspace/folder').fsPath);
1177
});
1178
1179
it('falls back to session state folder info', async () => {
1180
const sessionId = 'test-session';
1181
const token = disposables.add(new CancellationTokenSource()).token;
1182
1183
folderInfoMap.set(sessionId, { cwd: '/claude/project', additionalDirectories: [] });
1184
await fileSystem.createDirectory(URI.file('/claude/project'));
1185
1186
const result = await manager.getFolderRepository(sessionId, undefined, token);
1187
1188
expect(result.folder?.fsPath).toBe(vscode.Uri.file('/claude/project').fsPath);
1189
});
1190
1191
it('returns empty result when fallback folder does not exist', async () => {
1192
const sessionId = 'test-session';
1193
const token = disposables.add(new CancellationTokenSource()).token;
1194
1195
folderInfoMap.set(sessionId, { cwd: '/nonexistent/path', additionalDirectories: [] });
1196
1197
const result = await manager.getFolderRepository(sessionId, undefined, token);
1198
1199
expect(result.folder).toBeUndefined();
1200
});
1201
1202
it('returns empty result when no folder info available', async () => {
1203
const sessionId = 'unknown-session';
1204
const token = disposables.add(new CancellationTokenSource()).token;
1205
1206
const result = await manager.getFolderRepository(sessionId, undefined, token);
1207
1208
expect(result.folder).toBeUndefined();
1209
expect(result.repository).toBeUndefined();
1210
expect(result.worktree).toBeUndefined();
1211
});
1212
});
1213
});
1214
1215