Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeFolderMru.spec.ts
13406 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 } from '../../../../../platform/git/common/gitService';
8
import { RepositoryAccessDetails } from '../../../../../platform/git/vscode/git';
9
import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
10
import { mock } from '../../../../../util/common/test/simpleMock';
11
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
12
import { Event } from '../../../../../util/vs/base/common/event';
13
import { URI } from '../../../../../util/vs/base/common/uri';
14
import { IClaudeCodeSessionInfo } from '../sessionParser/claudeSessionSchema';
15
import { IClaudeCodeSessionService } from '../sessionParser/claudeCodeSessionService';
16
import { ClaudeCodeFolderMruService } from '../claudeCodeFolderMru';
17
18
// #region Test Helpers
19
20
class TestGitService extends mock<IGitService>() {
21
declare readonly _serviceBrand: undefined;
22
override onDidOpenRepository = Event.None;
23
override onDidCloseRepository = Event.None;
24
override onDidFinishInitialization = Event.None;
25
recentRepos: RepositoryAccessDetails[] = [];
26
override getRecentRepositories(): Iterable<RepositoryAccessDetails> {
27
return this.recentRepos;
28
}
29
}
30
31
class TestClaudeCodeSessionService extends mock<IClaudeCodeSessionService>() {
32
declare _serviceBrand: undefined;
33
sessions: IClaudeCodeSessionInfo[] = [];
34
override getAllSessions = vi.fn(async () => this.sessions);
35
}
36
37
function makeSession(overrides: Partial<IClaudeCodeSessionInfo> & { id: string }): IClaudeCodeSessionInfo {
38
return {
39
label: 'test',
40
created: 1000,
41
...overrides,
42
};
43
}
44
45
// #endregion
46
47
describe('ClaudeCodeFolderMruService', () => {
48
let sessionService: TestClaudeCodeSessionService;
49
let gitService: TestGitService;
50
let workspaceService: TestWorkspaceService;
51
let service: ClaudeCodeFolderMruService;
52
53
beforeEach(() => {
54
sessionService = new TestClaudeCodeSessionService();
55
gitService = new TestGitService();
56
workspaceService = new TestWorkspaceService([]);
57
service = new ClaudeCodeFolderMruService(sessionService, gitService, workspaceService);
58
});
59
60
// #region Session extraction
61
62
it('returns empty array when no sessions exist', async () => {
63
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
64
expect(result).toEqual([]);
65
});
66
67
it('converts session cwd to folder URI', async () => {
68
sessionService.sessions = [
69
makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 2000 }),
70
];
71
72
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
73
74
expect(result).toHaveLength(1);
75
expect(result[0].folder.toString()).toBe(URI.file('/Users/test/project').toString());
76
expect(result[0].lastAccessed).toBe(2000);
77
});
78
79
it('skips sessions without cwd', async () => {
80
sessionService.sessions = [makeSession({ id: 's1' })];
81
82
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
83
expect(result).toEqual([]);
84
});
85
86
it('skips sessions with .claude/worktrees/ cwd', async () => {
87
sessionService.sessions = [
88
makeSession({ id: 's1', cwd: '/Users/test/.claude/worktrees/branch-1' }),
89
];
90
91
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
92
expect(result).toEqual([]);
93
});
94
95
it('skips sessions with .worktrees/copilot- cwd', async () => {
96
sessionService.sessions = [
97
makeSession({ id: 's1', cwd: '/Users/test/repo/.worktrees/copilot-abc123' }),
98
];
99
100
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
101
expect(result).toEqual([]);
102
});
103
104
// #endregion
105
106
// #region Timestamp fallback
107
108
it('uses lastRequestEnded as primary timestamp', async () => {
109
sessionService.sessions = [
110
makeSession({ id: 's1', cwd: '/a', created: 100, lastRequestStarted: 200, lastRequestEnded: 300 }),
111
];
112
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
113
expect(result[0].lastAccessed).toBe(300);
114
});
115
116
it('falls back to lastRequestStarted', async () => {
117
sessionService.sessions = [
118
makeSession({ id: 's1', cwd: '/a', created: 100, lastRequestStarted: 200 }),
119
];
120
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
121
expect(result[0].lastAccessed).toBe(200);
122
});
123
124
it('falls back to created', async () => {
125
sessionService.sessions = [
126
makeSession({ id: 's1', cwd: '/a', created: 100 }),
127
];
128
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
129
expect(result[0].lastAccessed).toBe(100);
130
});
131
132
// #endregion
133
134
// #region Git repository merging
135
136
it('merges git repo into matching session entry', async () => {
137
sessionService.sessions = [
138
makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),
139
];
140
const folderUri = URI.file('/Users/test/project');
141
gitService.recentRepos = [{ rootUri: folderUri, lastAccessTime: 200 }];
142
143
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
144
145
expect(result).toHaveLength(1);
146
expect(result[0].repository).toEqual(folderUri);
147
expect(result[0].lastAccessed).toBe(200);
148
});
149
150
it('adds standalone git repos not in sessions', async () => {
151
const repoUri = URI.file('/Users/test/other-repo');
152
gitService.recentRepos = [{ rootUri: repoUri, lastAccessTime: 500 }];
153
154
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
155
156
expect(result).toHaveLength(1);
157
expect(result[0].folder).toEqual(repoUri);
158
expect(result[0].repository).toEqual(repoUri);
159
expect(result[0].lastAccessed).toBe(500);
160
});
161
162
it('filters git repos with .claude/worktrees/ path', async () => {
163
gitService.recentRepos = [
164
{ rootUri: URI.file('/Users/test/.claude/worktrees/branch'), lastAccessTime: 100 },
165
];
166
167
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
168
expect(result).toEqual([]);
169
});
170
171
it('filters git repos with .worktrees/copilot- path', async () => {
172
gitService.recentRepos = [
173
{ rootUri: URI.file('/Users/test/repo/.worktrees/copilot-abc123'), lastAccessTime: 100 },
174
];
175
176
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
177
expect(result).toEqual([]);
178
});
179
180
// #endregion
181
182
// #region Workspace folders
183
184
it('adds workspace folders not already present', async () => {
185
const folder = URI.file('/Users/test/workspace');
186
workspaceService = new TestWorkspaceService([folder]);
187
service = new ClaudeCodeFolderMruService(sessionService, gitService, workspaceService);
188
189
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
190
191
expect(result).toHaveLength(1);
192
expect(result[0].folder).toEqual(folder);
193
expect(result[0].repository).toBeUndefined();
194
});
195
196
it('does not duplicate workspace folders already in sessions', async () => {
197
const folder = URI.file('/Users/test/project');
198
sessionService.sessions = [
199
makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),
200
];
201
workspaceService = new TestWorkspaceService([folder]);
202
service = new ClaudeCodeFolderMruService(sessionService, gitService, workspaceService);
203
204
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
205
expect(result).toHaveLength(1);
206
});
207
208
// #endregion
209
210
// #region Sorting, caching, deletion
211
212
it('sorts entries by lastAccessed descending', async () => {
213
sessionService.sessions = [
214
makeSession({ id: 's1', cwd: '/Users/test/old', lastRequestEnded: 100 }),
215
makeSession({ id: 's2', cwd: '/Users/test/new', lastRequestEnded: 300 }),
216
makeSession({ id: 's3', cwd: '/Users/test/mid', lastRequestEnded: 200 }),
217
];
218
219
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
220
expect(result.map(e => e.lastAccessed)).toEqual([300, 200, 100]);
221
});
222
223
it('deleteRecentlyUsedFolder filters the folder from results', async () => {
224
const folder = URI.file('/Users/test/project');
225
sessionService.sessions = [
226
makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),
227
];
228
229
await service.deleteRecentlyUsedFolder(folder);
230
const result = await service.getRecentlyUsedFolders(CancellationToken.None);
231
expect(result).toEqual([]);
232
});
233
234
it('returns cached entries on subsequent calls', async () => {
235
sessionService.sessions = [
236
makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),
237
];
238
239
const first = await service.getRecentlyUsedFolders(CancellationToken.None);
240
expect(first).toHaveLength(1);
241
242
// Add another session — second call returns stale cache immediately
243
sessionService.sessions.push(
244
makeSession({ id: 's2', cwd: '/Users/test/other', lastRequestEnded: 200 }),
245
);
246
const second = await service.getRecentlyUsedFolders(CancellationToken.None);
247
expect(second).toHaveLength(1);
248
});
249
250
// #endregion
251
});
252
253