Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/fileEditTracker.test.ts
13399 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 assert from 'assert';
7
import { VSBuffer } from '../../../../base/common/buffer.js';
8
import { DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
11
import { FileService } from '../../../files/common/fileService.js';
12
import { IFileService } from '../../../files/common/files.js';
13
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
14
import { ILogService, NullLogService } from '../../../log/common/log.js';
15
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
16
import { InstantiationService } from '../../../instantiation/common/instantiationService.js';
17
import { ServiceCollection } from '../../../instantiation/common/serviceCollection.js';
18
import { IDiffComputeService } from '../../common/diffComputeService.js';
19
import { ToolResultContentType } from '../../common/state/sessionState.js';
20
import { createZeroDiffComputeService } from '../common/sessionTestHelpers.js';
21
import { SessionDatabase } from '../../node/sessionDatabase.js';
22
import { FileEditTracker, buildSessionDbUri, parseSessionDbUri } from '../../node/copilot/fileEditTracker.js';
23
24
suite('FileEditTracker', () => {
25
26
const disposables = new DisposableStore();
27
let fileService: FileService;
28
let db: SessionDatabase;
29
let tracker: FileEditTracker;
30
31
setup(async () => {
32
fileService = disposables.add(new FileService(new NullLogService()));
33
const sourceFs = disposables.add(new InMemoryFileSystemProvider());
34
disposables.add(fileService.registerProvider('file', sourceFs));
35
36
db = disposables.add(await SessionDatabase.open(':memory:'));
37
await db.createTurn('turn-1');
38
39
const services = new ServiceCollection();
40
services.set(ILogService, new NullLogService());
41
services.set(IFileService, fileService);
42
services.set(IDiffComputeService, createZeroDiffComputeService());
43
const instantiationService: IInstantiationService = disposables.add(new InstantiationService(services));
44
tracker = instantiationService.createInstance(FileEditTracker, 'copilot:/test-session', db);
45
});
46
47
teardown(async () => {
48
disposables.clear();
49
await db.close();
50
});
51
ensureNoDisposablesAreLeakedInTestSuite();
52
53
test('tracks edit start and complete for existing file', async () => {
54
await fileService.writeFile(URI.file('/workspace/test.txt'), VSBuffer.fromString('original content\nline 2'));
55
56
await tracker.trackEditStart('/workspace/test.txt');
57
await fileService.writeFile(URI.file('/workspace/test.txt'), VSBuffer.fromString('modified content\nline 2\nline 3'));
58
await tracker.completeEdit('/workspace/test.txt');
59
60
const fileEdit = await tracker.takeCompletedEdit('turn-1', 'tc-1', '/workspace/test.txt');
61
assert.ok(fileEdit);
62
assert.strictEqual(fileEdit.type, ToolResultContentType.FileEdit);
63
64
// URIs are parseable session-db: URIs
65
const beforeFields = parseSessionDbUri(fileEdit.before!.content.uri);
66
assert.ok(beforeFields);
67
assert.strictEqual(beforeFields.sessionUri, 'copilot:/test-session');
68
assert.strictEqual(beforeFields.toolCallId, 'tc-1');
69
assert.strictEqual(beforeFields.filePath, '/workspace/test.txt');
70
assert.strictEqual(beforeFields.part, 'before');
71
72
const afterFields = parseSessionDbUri(fileEdit.after!.content.uri);
73
assert.ok(afterFields);
74
assert.strictEqual(afterFields.part, 'after');
75
76
// Content is persisted in the database (wait for fire-and-forget write)
77
await new Promise(r => setTimeout(r, 50));
78
79
const content = await db.readFileEditContent('tc-1', '/workspace/test.txt');
80
assert.ok(content);
81
assert.strictEqual(new TextDecoder().decode(content.beforeContent), 'original content\nline 2');
82
assert.strictEqual(new TextDecoder().decode(content.afterContent), 'modified content\nline 2\nline 3');
83
});
84
85
test('tracks edit for newly created file (no before content)', async () => {
86
await tracker.trackEditStart('/workspace/new-file.txt');
87
await fileService.writeFile(URI.file('/workspace/new-file.txt'), VSBuffer.fromString('new file\ncontent'));
88
await tracker.completeEdit('/workspace/new-file.txt');
89
90
const fileEdit = await tracker.takeCompletedEdit('turn-1', 'tc-2', '/workspace/new-file.txt');
91
assert.ok(fileEdit);
92
93
// Wait for the fire-and-forget DB write to complete
94
await new Promise(r => setTimeout(r, 50));
95
96
const content = await db.readFileEditContent('tc-2', '/workspace/new-file.txt');
97
assert.ok(content);
98
assert.strictEqual(new TextDecoder().decode(content.beforeContent), '');
99
assert.strictEqual(new TextDecoder().decode(content.afterContent), 'new file\ncontent');
100
});
101
102
test('takeCompletedEdit returns undefined for unknown file path', async () => {
103
const result = await tracker.takeCompletedEdit('turn-1', 'tc-x', '/nonexistent');
104
assert.strictEqual(result, undefined);
105
});
106
107
test('before and after content can be read from database', async () => {
108
await fileService.writeFile(URI.file('/workspace/file.ts'), VSBuffer.fromString('original'));
109
110
await tracker.trackEditStart('/workspace/file.ts');
111
await fileService.writeFile(URI.file('/workspace/file.ts'), VSBuffer.fromString('modified'));
112
await tracker.completeEdit('/workspace/file.ts');
113
114
await tracker.takeCompletedEdit('turn-1', 'tc-3', '/workspace/file.ts');
115
116
// Wait for the fire-and-forget DB write to complete
117
await new Promise(r => setTimeout(r, 50));
118
119
const content = await db.readFileEditContent('tc-3', '/workspace/file.ts');
120
assert.ok(content);
121
assert.strictEqual(new TextDecoder().decode(content.beforeContent), 'original');
122
assert.strictEqual(new TextDecoder().decode(content.afterContent), 'modified');
123
});
124
});
125
126
suite('buildSessionDbUri / parseSessionDbUri', () => {
127
128
ensureNoDisposablesAreLeakedInTestSuite();
129
130
test('round-trips a simple URI', () => {
131
const uri = buildSessionDbUri('copilot:/abc-123', 'tc-1', '/workspace/file.ts', 'before');
132
const parsed = parseSessionDbUri(uri);
133
assert.ok(parsed);
134
assert.deepStrictEqual(parsed, {
135
sessionUri: 'copilot:/abc-123',
136
toolCallId: 'tc-1',
137
filePath: '/workspace/file.ts',
138
part: 'before',
139
});
140
});
141
142
test('round-trips with special characters in filePath', () => {
143
const uri = buildSessionDbUri('copilot:/s1', 'tc-2', '/work space/file (1).ts', 'after');
144
const parsed = parseSessionDbUri(uri);
145
assert.ok(parsed);
146
assert.strictEqual(parsed.filePath, '/work space/file (1).ts');
147
assert.strictEqual(parsed.part, 'after');
148
});
149
150
test('round-trips with special characters in toolCallId', () => {
151
const uri = buildSessionDbUri('copilot:/s1', 'call_abc=123&x', '/file.ts', 'before');
152
const parsed = parseSessionDbUri(uri);
153
assert.ok(parsed);
154
assert.strictEqual(parsed.toolCallId, 'call_abc=123&x');
155
});
156
157
test('parseSessionDbUri returns undefined for non-session-db URIs', () => {
158
assert.strictEqual(parseSessionDbUri('file:///foo/bar'), undefined);
159
assert.strictEqual(parseSessionDbUri('https://example.com'), undefined);
160
});
161
162
test('parseSessionDbUri returns undefined for malformed session-db URIs', () => {
163
assert.strictEqual(parseSessionDbUri('session-db:copilot:/s1'), undefined);
164
assert.strictEqual(parseSessionDbUri('session-db:copilot:/s1?toolCallId=tc-1'), undefined);
165
assert.strictEqual(parseSessionDbUri('session-db:copilot:/s1?toolCallId=tc-1&filePath=/f&part=middle'), undefined);
166
});
167
168
test('URI path ends with the basename of the file', () => {
169
const uri = buildSessionDbUri('copilot:/s1', 'tc-1', '/workspace/src/index.ts', 'before');
170
const parsed = URI.parse(uri);
171
assert.ok(parsed.path.endsWith('/index.ts'));
172
});
173
174
test('URI path ends with basename for files with spaces and special chars', () => {
175
const uri = buildSessionDbUri('copilot:/s1', 'tc-1', '/work space/file (1).ts', 'after');
176
const parsed = URI.parse(uri);
177
assert.ok(parsed.path.endsWith('/file (1).ts'));
178
});
179
});
180
181