Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.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 { DeferredPromise } from '../../../../base/common/async.js';
8
import { Emitter } from '../../../../base/common/event.js';
9
import { Disposable } from '../../../../base/common/lifecycle.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
12
import { FileService } from '../../../files/common/fileService.js';
13
import { NullLogService } from '../../../log/common/log.js';
14
import { RemoteAgentHostProtocolClient, RemoteAgentHostProtocolError } from '../../browser/remoteAgentHostProtocolClient.js';
15
import { AhpErrorCodes } from '../../common/state/protocol/errors.js';
16
import type { AhpServerNotification, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ProtocolMessage } from '../../common/state/sessionProtocol.js';
17
import type { IClientTransport, IProtocolTransport } from '../../common/state/sessionTransport.js';
18
19
type ProtocolTransportMessage = ProtocolMessage | AhpServerNotification | JsonRpcNotification | JsonRpcResponse | JsonRpcRequest;
20
21
class TestProtocolTransport extends Disposable implements IProtocolTransport {
22
private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());
23
readonly onMessage = this._onMessage.event;
24
25
private readonly _onClose = this._register(new Emitter<void>());
26
readonly onClose = this._onClose.event;
27
28
readonly sentMessages: ProtocolTransportMessage[] = [];
29
30
send(message: ProtocolTransportMessage): void {
31
this.sentMessages.push(message);
32
}
33
34
fireMessage(message: ProtocolMessage): void {
35
this._onMessage.fire(message);
36
}
37
38
fireClose(): void {
39
this._onClose.fire();
40
}
41
}
42
43
class TestClientProtocolTransport extends TestProtocolTransport implements IClientTransport {
44
readonly connectDeferred = new DeferredPromise<void>();
45
46
connect(): Promise<void> {
47
return this.connectDeferred.p;
48
}
49
}
50
51
class CloseOnDisposeProtocolTransport extends TestProtocolTransport {
52
override dispose(): void {
53
this.fireClose();
54
super.dispose();
55
}
56
}
57
58
suite('RemoteAgentHostProtocolClient', () => {
59
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
60
61
function createClient(transport = disposables.add(new TestProtocolTransport())): { client: RemoteAgentHostProtocolClient; transport: TestProtocolTransport } {
62
const fileService = disposables.add(new FileService(new NullLogService()));
63
const client = disposables.add(new RemoteAgentHostProtocolClient('test.example:1234', transport, new NullLogService(), fileService));
64
return { client, transport };
65
}
66
67
async function assertRemoteProtocolError(promise: Promise<unknown>, expected: { code: number; message: string; data?: unknown }): Promise<void> {
68
try {
69
await promise;
70
assert.fail('Expected promise to reject');
71
} catch (error) {
72
if (!(error instanceof RemoteAgentHostProtocolError)) {
73
assert.fail(`Expected RemoteAgentHostProtocolError, got ${String(error)}`);
74
}
75
assert.strictEqual(error.code, expected.code);
76
assert.strictEqual(error.message, expected.message);
77
assert.deepStrictEqual(error.data, expected.data);
78
}
79
}
80
81
test('completes matching response and removes it from pending requests', async () => {
82
const { client, transport } = createClient();
83
const resultPromise = client.resourceList(URI.file('/workspace'));
84
85
assert.deepStrictEqual(transport.sentMessages[0], {
86
jsonrpc: '2.0',
87
id: 1,
88
method: 'resourceList',
89
params: { uri: URI.file('/workspace').toString() },
90
});
91
92
transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [] } });
93
assert.deepStrictEqual(await resultPromise, { entries: [] });
94
95
transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [{ name: 'late', type: 'file' }] } });
96
assert.strictEqual(transport.sentMessages.length, 1);
97
});
98
99
test('preserves JSON-RPC error code and data', async () => {
100
const { client, transport } = createClient();
101
const resultPromise = client.resourceRead(URI.file('/missing'));
102
const data = { uri: URI.file('/missing').toString() };
103
104
transport.fireMessage({ jsonrpc: '2.0', id: 1, error: { code: AhpErrorCodes.NotFound, message: 'Missing resource', data } });
105
106
await assertRemoteProtocolError(resultPromise, { code: AhpErrorCodes.NotFound, message: 'Missing resource', data });
107
});
108
109
test('ignores response for unknown request id', () => {
110
const { transport } = createClient();
111
112
transport.fireMessage({ jsonrpc: '2.0', id: 99, result: null });
113
114
assert.strictEqual(transport.sentMessages.length, 0);
115
});
116
117
test('rejects all pending requests on transport close', async () => {
118
const { client, transport } = createClient();
119
const first = client.resourceList(URI.file('/one'));
120
const second = client.resourceRead(URI.file('/two'));
121
let closeCount = 0;
122
disposables.add(client.onDidClose(() => closeCount++));
123
const firstRejected = assertRemoteProtocolError(first, { code: -32000, message: 'Connection closed: test.example:1234' });
124
const secondRejected = assertRemoteProtocolError(second, { code: -32000, message: 'Connection closed: test.example:1234' });
125
126
transport.fireClose();
127
transport.fireClose();
128
129
await firstRejected;
130
await secondRejected;
131
assert.strictEqual(closeCount, 1);
132
});
133
134
test('rejects pending requests on dispose', async () => {
135
const { client } = createClient();
136
const resultPromise = client.resourceList(URI.file('/workspace'));
137
const rejected = assertRemoteProtocolError(resultPromise, { code: -32000, message: 'Connection disposed: test.example:1234' });
138
139
client.dispose();
140
141
await rejected;
142
});
143
144
test('dispose rejection wins when transport emits close while disposing', async () => {
145
const transport = disposables.add(new CloseOnDisposeProtocolTransport());
146
const { client } = createClient(transport);
147
const resultPromise = client.resourceList(URI.file('/workspace'));
148
const rejected = assertRemoteProtocolError(resultPromise, { code: -32000, message: 'Connection disposed: test.example:1234' });
149
150
client.dispose();
151
152
await rejected;
153
});
154
155
test('late response after close does not complete rejected request', async () => {
156
const { client, transport } = createClient();
157
const resultPromise = client.resourceList(URI.file('/workspace'));
158
const rejected = assertRemoteProtocolError(resultPromise, { code: -32000, message: 'Connection closed: test.example:1234' });
159
160
transport.fireClose();
161
transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [] } });
162
163
await rejected;
164
});
165
166
test('rejects requests started after transport close', async () => {
167
const { client, transport } = createClient();
168
169
transport.fireClose();
170
171
await assertRemoteProtocolError(client.resourceList(URI.file('/workspace')), { code: -32000, message: 'Connection closed: test.example:1234' });
172
assert.strictEqual(transport.sentMessages.length, 0);
173
});
174
175
test('rejects requests started after dispose', async () => {
176
const { client, transport } = createClient();
177
178
client.dispose();
179
180
await assertRemoteProtocolError(client.resourceList(URI.file('/workspace')), { code: -32000, message: 'Connection disposed: test.example:1234' });
181
assert.strictEqual(transport.sentMessages.length, 0);
182
});
183
184
test('rejects connect when transport closes before connect completes', async () => {
185
const transport = disposables.add(new TestClientProtocolTransport());
186
const { client } = createClient(transport);
187
const rejected = assertRemoteProtocolError(client.connect(), { code: -32000, message: 'Connection closed: test.example:1234' });
188
189
transport.fireClose();
190
transport.connectDeferred.complete();
191
192
await rejected;
193
assert.strictEqual(transport.sentMessages.length, 0);
194
});
195
196
test('rejects connect when disposed before transport connect completes', async () => {
197
const transport = disposables.add(new TestClientProtocolTransport());
198
const { client } = createClient(transport);
199
const rejected = assertRemoteProtocolError(client.connect(), { code: -32000, message: 'Connection disposed: test.example:1234' });
200
201
client.dispose();
202
203
await rejected;
204
assert.strictEqual(transport.sentMessages.length, 0);
205
});
206
207
test('sends shutdown as a JSON-RPC request shape', async () => {
208
const { client, transport } = createClient();
209
const resultPromise = client.shutdown();
210
211
assert.deepStrictEqual(transport.sentMessages[0], {
212
jsonrpc: '2.0',
213
id: 1,
214
method: 'shutdown',
215
params: undefined,
216
});
217
218
transport.fireMessage({ jsonrpc: '2.0', id: 1, result: null });
219
await resultPromise;
220
});
221
222
test('rejects shutdown with structured JSON-RPC error', async () => {
223
const { client, transport } = createClient();
224
const resultPromise = client.shutdown();
225
226
transport.fireMessage({ jsonrpc: '2.0', id: 1, error: { code: AhpErrorCodes.TurnInProgress, message: 'Turn in progress' } });
227
228
await assertRemoteProtocolError(resultPromise, { code: AhpErrorCodes.TurnInProgress, message: 'Turn in progress' });
229
});
230
});
231
232