Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/test/chatDiskSessionResources.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, test, vi } from 'vitest';
7
import { FileType } from '../../../../platform/filesystem/common/fileTypes';
8
import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService';
9
import { TestLogService } from '../../../../platform/testing/common/testLogService';
10
import { URI } from '../../../../util/vs/base/common/uri';
11
import { ChatDiskSessionResources } from '../chatDiskSessionResourcesImpl';
12
13
/**
14
* Mock extension context with a storage URI configured.
15
*/
16
class MockExtensionContextWithStorage {
17
readonly storageUri = URI.file('/test-storage');
18
}
19
20
// Constants matching the implementation
21
const RETENTION_PERIOD_MS = 8 * 60 * 60 * 1000; // 8 hours
22
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
23
24
describe('ChatDiskSessionResources', () => {
25
let mockFs: MockFileSystemService;
26
let service: ChatDiskSessionResources;
27
28
beforeEach(() => {
29
mockFs = new MockFileSystemService();
30
const mockContext = new MockExtensionContextWithStorage();
31
const logService = new TestLogService();
32
33
// Mock the storage directory
34
mockFs.mockDirectory(mockContext.storageUri, []);
35
36
// Create the service directly with mocked dependencies
37
service = new ChatDiskSessionResources(
38
mockContext as any,
39
mockFs,
40
logService
41
);
42
});
43
44
afterEach(() => {
45
service.dispose();
46
vi.resetAllMocks();
47
});
48
49
describe('ensure', () => {
50
test('creates file with string content', async () => {
51
const sessionId = 'session-123';
52
const subdir = 'tool-result-1';
53
const content = 'Hello, world!';
54
55
const resultUri = await service.ensure(sessionId, subdir, content);
56
57
expect(resultUri).toBeDefined();
58
expect(resultUri.path).toContain('session-123');
59
expect(resultUri.path).toContain('tool-result-1');
60
});
61
62
test('sanitizes session ID and subdir with special characters', async () => {
63
const sessionId = 'session/with:special*chars';
64
const subdir = 'tool<result>';
65
const content = 'Test content';
66
67
const resultUri = await service.ensure(sessionId, subdir, content);
68
69
// The path should contain sanitized versions (special chars replaced with underscores)
70
expect(resultUri.path).toContain('session_with_special_chars');
71
expect(resultUri.path).toContain('tool_result_');
72
});
73
74
test('creates file tree with nested structure', async () => {
75
const sessionId = 'session-456';
76
const subdir = 'complex-result';
77
const files = {
78
'readme.txt': 'This is a readme',
79
'src': {
80
'main.ts': 'console.log("hello")',
81
'utils': {
82
'helper.ts': 'export function help() {}'
83
}
84
}
85
};
86
87
const resultUri = await service.ensure(sessionId, subdir, files);
88
89
expect(resultUri).toBeDefined();
90
expect(resultUri.path).toContain('session-456');
91
});
92
93
test('is idempotent for same content', async () => {
94
const sessionId = 'session-789';
95
const subdir = 'idempotent-test';
96
const content = 'Same content';
97
98
const uri1 = await service.ensure(sessionId, subdir, content);
99
const uri2 = await service.ensure(sessionId, subdir, content);
100
101
expect(uri1.toString()).toBe(uri2.toString());
102
});
103
});
104
105
describe('isSessionResourceUri', () => {
106
test('returns true for URIs within storage directory', async () => {
107
const sessionId = 'session-abc';
108
const subdir = 'test-subdir';
109
const content = 'Test';
110
111
const resultUri = await service.ensure(sessionId, subdir, content);
112
113
expect(service.isSessionResourceUri(resultUri)).toBe(true);
114
});
115
116
test('returns false for URIs outside storage directory', () => {
117
const externalUri = URI.file('/some/other/path');
118
119
expect(service.isSessionResourceUri(externalUri)).toBe(false);
120
});
121
122
test('returns false for workspace URIs', () => {
123
const workspaceUri = URI.file('/workspace/project/file.ts');
124
125
expect(service.isSessionResourceUri(workspaceUri)).toBe(false);
126
});
127
});
128
129
describe('path sanitization', () => {
130
test('preserves alphanumeric characters', async () => {
131
const sessionId = 'abc123XYZ';
132
const subdir = 'test456';
133
const content = 'Test';
134
135
const resultUri = await service.ensure(sessionId, subdir, content);
136
137
expect(resultUri.path).toContain('abc123XYZ');
138
expect(resultUri.path).toContain('test456');
139
});
140
141
test('preserves underscores and hyphens', async () => {
142
const sessionId = 'session_with-dashes';
143
const subdir = 'tool_result-1';
144
const content = 'Test';
145
146
const resultUri = await service.ensure(sessionId, subdir, content);
147
148
expect(resultUri.path).toContain('session_with-dashes');
149
expect(resultUri.path).toContain('tool_result-1');
150
});
151
152
test('handles empty strings after sanitization gracefully', async () => {
153
const sessionId = '';
154
const subdir = '';
155
const content = 'Test';
156
157
const resultUri = await service.ensure(sessionId, subdir, content);
158
159
// Should still create a valid path
160
expect(resultUri).toBeDefined();
161
expect(resultUri.path).toContain('chat-session-resources');
162
});
163
});
164
165
describe('file content handling', () => {
166
test('handles empty string content', async () => {
167
const sessionId = 'session-empty';
168
const subdir = 'empty-content';
169
const content = '';
170
171
const resultUri = await service.ensure(sessionId, subdir, content);
172
173
expect(resultUri).toBeDefined();
174
});
175
176
test('handles large content', async () => {
177
const sessionId = 'session-large';
178
const subdir = 'large-content';
179
const content = 'x'.repeat(100000); // 100KB of content
180
181
const resultUri = await service.ensure(sessionId, subdir, content);
182
183
expect(resultUri).toBeDefined();
184
});
185
186
test('handles unicode content', async () => {
187
const sessionId = 'session-unicode';
188
const subdir = 'unicode-content';
189
const content = '你好世界 🌍 مرحبا';
190
191
const resultUri = await service.ensure(sessionId, subdir, content);
192
193
expect(resultUri).toBeDefined();
194
});
195
});
196
197
describe('cleanup and expiration', () => {
198
let mockContext: MockExtensionContextWithStorage;
199
let logService: TestLogService;
200
201
beforeEach(() => {
202
vi.useFakeTimers({ shouldAdvanceTime: false });
203
mockFs = new MockFileSystemService();
204
mockContext = new MockExtensionContextWithStorage();
205
logService = new TestLogService();
206
// Mock the storage directory AND the session resources subdirectory
207
mockFs.mockDirectory(mockContext.storageUri, [['chat-session-resources', FileType.Directory]]);
208
mockFs.mockDirectory(URI.joinPath(mockContext.storageUri, 'chat-session-resources'), []);
209
});
210
211
afterEach(() => {
212
vi.useRealTimers();
213
});
214
215
test('cleanup runs on scheduled interval', async () => {
216
// Set a specific start time
217
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
218
219
const testService = new ChatDiskSessionResources(
220
mockContext as any,
221
mockFs,
222
logService
223
);
224
225
// Create a resource at time T=0
226
const resultUri = await testService.ensure('session-1', 'tool-1', 'content');
227
228
// Verify directory exists
229
const stat = await mockFs.stat(resultUri);
230
expect(stat.type).toBe(FileType.Directory);
231
232
// Advance time past retention period AND cleanup interval
233
await vi.advanceTimersByTimeAsync(RETENTION_PERIOD_MS + CLEANUP_INTERVAL_MS + 1000);
234
await testService.currentCleanup;
235
236
// The directory should be cleaned up now (cleanup deletes at directory level)
237
await expect(mockFs.stat(resultUri)).rejects.toThrow();
238
239
testService.dispose();
240
});
241
242
test('recently accessed resources are not cleaned up', async () => {
243
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
244
245
const testService = new ChatDiskSessionResources(
246
mockContext as any,
247
mockFs,
248
logService
249
);
250
251
// Create a resource
252
const resultUri = await testService.ensure('session-fresh', 'tool-fresh', 'fresh content');
253
254
// Advance time but not past retention
255
await vi.advanceTimersByTimeAsync(RETENTION_PERIOD_MS / 2);
256
257
// Trigger cleanup
258
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
259
260
// Fresh resource should still exist
261
const contentUri = URI.joinPath(resultUri, 'content.txt');
262
const stat = await mockFs.stat(contentUri);
263
expect(stat.type).toBe(FileType.File);
264
265
testService.dispose();
266
});
267
268
test('resources older than retention period are cleaned up', async () => {
269
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
270
271
const testService = new ChatDiskSessionResources(
272
mockContext as any,
273
mockFs,
274
logService
275
);
276
277
// Create a resource
278
const resultUri = await testService.ensure('session-old', 'tool-old', 'old content');
279
280
// Verify directory exists initially
281
const stat = await mockFs.stat(resultUri);
282
expect(stat.type).toBe(FileType.Directory);
283
284
// Advance time past retention period AND trigger cleanup
285
await vi.advanceTimersByTimeAsync(RETENTION_PERIOD_MS + CLEANUP_INTERVAL_MS + 1000);
286
await testService.currentCleanup;
287
288
// Old resource directory should be cleaned up
289
await expect(mockFs.stat(resultUri)).rejects.toThrow();
290
291
testService.dispose();
292
});
293
294
test('empty session directories are removed during cleanup', async () => {
295
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
296
297
const testService = new ChatDiskSessionResources(
298
mockContext as any,
299
mockFs,
300
logService
301
);
302
303
// Create a resource
304
await testService.ensure('session-empty-dir', 'tool-1', 'content');
305
306
// Advance time past retention to trigger cleanup of the tool
307
await vi.advanceTimersByTimeAsync(RETENTION_PERIOD_MS + CLEANUP_INTERVAL_MS + 1000);
308
309
await testService.currentCleanup;
310
311
// The session directory should be gone since the tool was cleaned up
312
const sessionUri = URI.joinPath(mockContext.storageUri, 'chat-session-resources', 'session-empty-dir');
313
await expect(mockFs.stat(sessionUri)).rejects.toThrow();
314
315
testService.dispose();
316
});
317
318
test('dispose cancels cleanup timer', async () => {
319
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
320
321
const testService = new ChatDiskSessionResources(
322
mockContext as any,
323
mockFs,
324
logService
325
);
326
327
// Create a resource
328
const resultUri = URI.joinPath(
329
mockContext.storageUri,
330
'chat-session-resources',
331
'session-dispose',
332
'tool-1',
333
'content.txt'
334
);
335
await testService.ensure('session-dispose', 'tool-1', 'content');
336
337
// Dispose the service BEFORE advancing time
338
testService.dispose();
339
340
// Advance time past retention + cleanup interval
341
await vi.advanceTimersByTimeAsync(RETENTION_PERIOD_MS + CLEANUP_INTERVAL_MS + 1000);
342
343
await testService.currentCleanup;
344
345
// Resource should still exist because cleanup was cancelled
346
const stat = await mockFs.stat(resultUri);
347
expect(stat.type).toBe(FileType.File);
348
});
349
350
test('accessing resource resets its expiration timer', async () => {
351
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
352
353
const testService = new ChatDiskSessionResources(
354
mockContext as any,
355
mockFs,
356
logService
357
);
358
359
// Create a resource at T=0
360
const resultUri = await testService.ensure('session-refresh', 'tool-refresh', 'content v1');
361
362
// Advance time to just before retention expires (7.9 hours)
363
await vi.advanceTimersByTimeAsync(RETENTION_PERIOD_MS - 1000);
364
365
// Access/update the resource - this should reset the access timestamp
366
await testService.ensure('session-refresh', 'tool-refresh', 'content v2');
367
368
// Advance time past what would have been the original expiration + cleanup
369
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS + 2000);
370
371
await testService.currentCleanup;
372
373
// Resource should still exist because it was refreshed
374
const contentUri = URI.joinPath(resultUri, 'content.txt');
375
const stat = await mockFs.stat(contentUri);
376
expect(stat.type).toBe(FileType.File);
377
378
testService.dispose();
379
});
380
});
381
});
382
383
384