Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/common/agentHostFileSystemProvider.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 { URI } from '../../../../base/common/uri.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
10
import { FileType } from '../../../files/common/files.js';
11
import { AgentHostFileSystemProvider, agentHostRemotePath, agentHostUri, type IRemoteFilesystemConnection } from '../../common/agentHostFileSystemProvider.js';
12
import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME, agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../../common/agentHostUri.js';
13
import { ContentEncoding, type ResourceListResult, type ResourceReadResult } from '../../common/state/protocol/commands.js';
14
15
suite('AgentHostFileSystemProvider - URI helpers', () => {
16
17
ensureNoDisposablesAreLeakedInTestSuite();
18
19
test('agentHostUri builds correct URI', () => {
20
const uri = agentHostUri('localhost', '/home/user/project');
21
assert.strictEqual(uri.scheme, AGENT_HOST_SCHEME);
22
assert.strictEqual(uri.authority, 'localhost');
23
// path encodes file scheme: /file//home/user/project
24
assert.ok(uri.path.includes('/home/user/project'));
25
});
26
27
test('agentHostRemotePath extracts the original path', () => {
28
const uri = agentHostUri('host', '/some/path');
29
assert.strictEqual(agentHostRemotePath(uri), '/some/path');
30
});
31
32
test('agentHostRemotePath round-trips with agentHostUri', () => {
33
const original = '/home/user/project';
34
const uri = agentHostUri('host', original);
35
assert.strictEqual(agentHostRemotePath(uri), original);
36
});
37
});
38
39
suite('AgentHostAuthority - encoding', () => {
40
41
ensureNoDisposablesAreLeakedInTestSuite();
42
43
test('purely alphanumeric address is returned as-is', () => {
44
assert.strictEqual(agentHostAuthority('localhost'), 'localhost');
45
});
46
47
test('normal host:port address uses human-readable encoding', () => {
48
assert.strictEqual(agentHostAuthority('localhost:8081'), 'localhost__8081');
49
assert.strictEqual(agentHostAuthority('192.168.1.1:8080'), '192.168.1.1__8080');
50
assert.strictEqual(agentHostAuthority('my-host:9090'), 'my-host__9090');
51
assert.strictEqual(agentHostAuthority('host.name:80'), 'host.name__80');
52
});
53
54
test('address with underscore falls through to base64', () => {
55
const authority = agentHostAuthority('host_name:8080');
56
assert.ok(authority.startsWith('b64-'), `expected base64 for underscore address, got: ${authority}`);
57
});
58
59
test('address with exotic characters is base64-encoded', () => {
60
assert.ok(agentHostAuthority('user@host:8080').startsWith('b64-'));
61
assert.ok(agentHostAuthority('host with spaces').startsWith('b64-'));
62
assert.ok(agentHostAuthority('http://myhost:3000').startsWith('b64-'));
63
});
64
65
test('ws:// prefix is normalized so authority matches bare address', () => {
66
assert.strictEqual(agentHostAuthority('ws://127.0.0.1:8080'), agentHostAuthority('127.0.0.1:8080'));
67
assert.strictEqual(agentHostAuthority('ws://localhost:9090'), agentHostAuthority('localhost:9090'));
68
});
69
70
test('different addresses produce different authorities', () => {
71
const cases = ['localhost:8080', 'localhost:8081', '192.168.1.1:8080', 'host-name:80', 'host.name:80', 'host_name:80', 'user@host:8080'];
72
const results = cases.map(agentHostAuthority);
73
const unique = new Set(results);
74
assert.strictEqual(unique.size, cases.length, 'all authorities must be unique');
75
});
76
77
test('authority is valid in a URI authority position', () => {
78
const addresses = ['localhost', 'localhost:8081', 'user@host:8080', 'host with spaces', '192.168.1.1:9090'];
79
for (const address of addresses) {
80
const authority = agentHostAuthority(address);
81
const uri = URI.from({ scheme: AGENT_HOST_SCHEME, authority, path: '/test' });
82
assert.strictEqual(uri.authority, authority, `authority for '${address}' must round-trip through URI`);
83
}
84
});
85
86
test('authority is valid in a URI scheme position', () => {
87
const addresses = ['localhost', 'localhost:8081', 'user@host:8080', 'host with spaces'];
88
for (const address of addresses) {
89
const authority = agentHostAuthority(address);
90
const scheme = `remote-${authority}-copilot`;
91
const uri = URI.from({ scheme, path: '/test' });
92
assert.strictEqual(uri.scheme, scheme, `scheme for '${address}' must round-trip through URI`);
93
}
94
});
95
});
96
97
suite('toAgentHostUri / fromAgentHostUri', () => {
98
99
ensureNoDisposablesAreLeakedInTestSuite();
100
101
test('round-trips a file URI', () => {
102
const original = URI.file('/home/user/project/file.ts');
103
const wrapped = toAgentHostUri(original, 'my-server');
104
assert.strictEqual(wrapped.scheme, AGENT_HOST_SCHEME);
105
assert.strictEqual(wrapped.authority, 'my-server');
106
107
const unwrapped = fromAgentHostUri(wrapped);
108
assert.strictEqual(unwrapped.scheme, 'file');
109
assert.strictEqual(unwrapped.path, original.path);
110
});
111
112
test('round-trips a URI with authority', () => {
113
const original = URI.from({ scheme: 'agenthost-content', authority: 'session1', path: '/snap/before' });
114
const wrapped = toAgentHostUri(original, 'remote-host');
115
const unwrapped = fromAgentHostUri(wrapped);
116
assert.strictEqual(unwrapped.scheme, 'agenthost-content');
117
assert.strictEqual(unwrapped.authority, 'session1');
118
assert.strictEqual(unwrapped.path, '/snap/before');
119
});
120
121
test('local authority returns original URI unchanged', () => {
122
const original = URI.file('/workspace/test.ts');
123
const result = toAgentHostUri(original, 'local');
124
assert.strictEqual(result.toString(), original.toString());
125
});
126
127
test('agentHostUri for root path produces valid encoded URI', () => {
128
const authority = agentHostAuthority('localhost:8089');
129
const uri = agentHostUri(authority, '/');
130
assert.strictEqual(uri.scheme, AGENT_HOST_SCHEME);
131
assert.strictEqual(uri.authority, authority);
132
// The decoded path should be root
133
assert.strictEqual(fromAgentHostUri(uri).path, '/');
134
});
135
136
test('fromAgentHostUri handles malformed path gracefully', () => {
137
const uri = URI.from({ scheme: AGENT_HOST_SCHEME, authority: 'host', path: '/file' });
138
const result = fromAgentHostUri(uri);
139
// Should not throw - falls back to extracting scheme only
140
assert.strictEqual(result.scheme, 'file');
141
});
142
});
143
144
suite('AGENT_HOST_LABEL_FORMATTER', () => {
145
146
ensureNoDisposablesAreLeakedInTestSuite();
147
148
/**
149
* Replicates the stripPathSegments logic from the label service to
150
* verify that the formatter's configuration is consistent with the
151
* URI encoding.
152
*/
153
function stripPath(path: string, segments: number): string {
154
let pos = 0;
155
for (let i = 0; i < segments; i++) {
156
const next = path.indexOf('/', pos + 1);
157
if (next === -1) {
158
break;
159
}
160
pos = next;
161
}
162
return path.substring(pos);
163
}
164
165
test('stripPathSegments matches URI encoding for file URIs', () => {
166
const authority = agentHostAuthority('localhost:8089');
167
const originalPath = '/Users/roblou/code/vscode';
168
const encodedUri = agentHostUri(authority, originalPath);
169
170
const stripped = stripPath(encodedUri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!);
171
assert.strictEqual(stripped, originalPath);
172
});
173
174
test('stripPathSegments matches URI encoding with authority', () => {
175
const originalUri = URI.from({ scheme: 'agenthost-content', authority: 'myhost', path: '/snap/before' });
176
const encodedUri = toAgentHostUri(originalUri, 'remote-host');
177
178
const stripped = stripPath(encodedUri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!);
179
assert.strictEqual(stripped, '/snap/before');
180
});
181
});
182
183
suite('AgentHostFileSystemProvider - synthetic content schemes', () => {
184
185
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
186
187
/**
188
* Stub connection that records the URIs it's asked about and returns
189
* canned data, so we can assert on the URIs the provider passes through.
190
*/
191
class StubConnection implements IRemoteFilesystemConnection {
192
readonly readCalls: URI[] = [];
193
readonly listCalls: URI[] = [];
194
readResult: ResourceReadResult = { data: 'stub-content', encoding: ContentEncoding.Utf8, contentType: 'text/plain' };
195
196
async resourceRead(uri: URI): Promise<ResourceReadResult> {
197
this.readCalls.push(uri);
198
return this.readResult;
199
}
200
async resourceList(uri: URI): Promise<ResourceListResult> {
201
this.listCalls.push(uri);
202
return { entries: [] };
203
}
204
async resourceWrite(): Promise<{}> { return {}; }
205
async resourceDelete(): Promise<{}> { return {}; }
206
async resourceMove(): Promise<{}> { return {}; }
207
}
208
209
function setup() {
210
const provider = disposables.add(new AgentHostFileSystemProvider());
211
const connection = new StubConnection();
212
disposables.add(provider.registerAuthority('local', connection));
213
return { provider, connection };
214
}
215
216
// Regression: AHPFileSystemProvider.stat() used to fall through to
217
// _listDirectory(parent) for any URI whose decoded scheme wasn't
218
// session-db, which fails with "Directory not found" for synthetic
219
// content URIs that have no real parent directory. The diff editor
220
// stats every URI before reading it, so this broke "open diff of a
221
// modified file" entirely. The fix is the scheme allowlist in stat().
222
223
test('stat returns File for git-blob: URIs without listing the parent', async () => {
224
const { provider, connection } = setup();
225
const inner = URI.from({ scheme: 'git-blob', authority: 'sess1', path: '/sha/encoded/file.ts' });
226
const wrapped = toAgentHostUri(inner, 'local');
227
228
const stat = await provider.stat(wrapped);
229
230
assert.strictEqual(stat.type, FileType.File);
231
assert.deepStrictEqual(connection.listCalls, [], 'stat must not list a synthetic parent directory');
232
});
233
234
test('stat returns File for session-db: URIs (parity with git-blob)', async () => {
235
const { provider, connection } = setup();
236
const inner = URI.from({ scheme: 'session-db', authority: 'sess1', path: '/snap/some-blob' });
237
const wrapped = toAgentHostUri(inner, 'local');
238
239
const stat = await provider.stat(wrapped);
240
241
assert.strictEqual(stat.type, FileType.File);
242
assert.deepStrictEqual(connection.listCalls, []);
243
});
244
245
test('stat still lists parent for ordinary file: URIs', async () => {
246
// Use a non-local authority so the URI actually goes through the
247
// agent-host wrapping (toAgentHostUri short-circuits 'local'
248
// + file:// to return the URI unchanged).
249
const provider = disposables.add(new AgentHostFileSystemProvider());
250
const connection = new StubConnection();
251
disposables.add(provider.registerAuthority('remote', connection));
252
const wrapped = agentHostUri('remote', '/some/file.ts');
253
254
try {
255
await provider.stat(wrapped);
256
} catch {
257
// Either FileNotFound or EntryNotFound is fine — we only
258
// care that the provider tried to list the parent (rather
259
// than treating this as a synthetic content URI).
260
}
261
assert.strictEqual(connection.listCalls.length, 1);
262
});
263
264
test('readFile passes the decoded synthetic URI through to the connection', async () => {
265
const { provider, connection } = setup();
266
const inner = URI.from({ scheme: 'git-blob', authority: 'sess1', path: '/sha/encoded/file.ts' });
267
const wrapped = toAgentHostUri(inner, 'local');
268
269
const bytes = await provider.readFile(wrapped);
270
271
assert.strictEqual(VSBuffer.wrap(bytes).toString(), 'stub-content');
272
assert.deepStrictEqual(connection.readCalls.map(u => u.toString()), [inner.toString()]);
273
});
274
275
test('full stat-then-read round-trip mirrors the diff editor flow', async () => {
276
// This is the exact sequence the workbench's TextFileEditorModel
277
// goes through when DiffEditorInput.createModel resolves: stat
278
// the URI, then read the file. Pre-fix this combo failed at the
279
// stat step before readFile was even called.
280
const { provider } = setup();
281
const inner = URI.from({ scheme: 'git-blob', authority: 'sess1', path: '/sha/encoded/file.ts' });
282
const wrapped = toAgentHostUri(inner, 'local');
283
284
const stat = await provider.stat(wrapped);
285
assert.strictEqual(stat.type, FileType.File);
286
const bytes = await provider.readFile(wrapped);
287
assert.strictEqual(VSBuffer.wrap(bytes).toString(), 'stub-content');
288
});
289
});
290
291