Path: blob/main/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { DeferredPromise } from '../../../../base/common/async.js';7import { Emitter } from '../../../../base/common/event.js';8import { Disposable } from '../../../../base/common/lifecycle.js';9import { URI } from '../../../../base/common/uri.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';11import { FileService } from '../../../files/common/fileService.js';12import { NullLogService } from '../../../log/common/log.js';13import { RemoteAgentHostProtocolClient, RemoteAgentHostProtocolError } from '../../browser/remoteAgentHostProtocolClient.js';14import { AhpErrorCodes } from '../../common/state/protocol/errors.js';15import type { AhpServerNotification, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ProtocolMessage } from '../../common/state/sessionProtocol.js';16import type { IClientTransport, IProtocolTransport } from '../../common/state/sessionTransport.js';1718type ProtocolTransportMessage = ProtocolMessage | AhpServerNotification | JsonRpcNotification | JsonRpcResponse | JsonRpcRequest;1920class TestProtocolTransport extends Disposable implements IProtocolTransport {21private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());22readonly onMessage = this._onMessage.event;2324private readonly _onClose = this._register(new Emitter<void>());25readonly onClose = this._onClose.event;2627readonly sentMessages: ProtocolTransportMessage[] = [];2829send(message: ProtocolTransportMessage): void {30this.sentMessages.push(message);31}3233fireMessage(message: ProtocolMessage): void {34this._onMessage.fire(message);35}3637fireClose(): void {38this._onClose.fire();39}40}4142class TestClientProtocolTransport extends TestProtocolTransport implements IClientTransport {43readonly connectDeferred = new DeferredPromise<void>();4445connect(): Promise<void> {46return this.connectDeferred.p;47}48}4950class CloseOnDisposeProtocolTransport extends TestProtocolTransport {51override dispose(): void {52this.fireClose();53super.dispose();54}55}5657suite('RemoteAgentHostProtocolClient', () => {58const disposables = ensureNoDisposablesAreLeakedInTestSuite();5960function createClient(transport = disposables.add(new TestProtocolTransport())): { client: RemoteAgentHostProtocolClient; transport: TestProtocolTransport } {61const fileService = disposables.add(new FileService(new NullLogService()));62const client = disposables.add(new RemoteAgentHostProtocolClient('test.example:1234', transport, new NullLogService(), fileService));63return { client, transport };64}6566async function assertRemoteProtocolError(promise: Promise<unknown>, expected: { code: number; message: string; data?: unknown }): Promise<void> {67try {68await promise;69assert.fail('Expected promise to reject');70} catch (error) {71if (!(error instanceof RemoteAgentHostProtocolError)) {72assert.fail(`Expected RemoteAgentHostProtocolError, got ${String(error)}`);73}74assert.strictEqual(error.code, expected.code);75assert.strictEqual(error.message, expected.message);76assert.deepStrictEqual(error.data, expected.data);77}78}7980test('completes matching response and removes it from pending requests', async () => {81const { client, transport } = createClient();82const resultPromise = client.resourceList(URI.file('/workspace'));8384assert.deepStrictEqual(transport.sentMessages[0], {85jsonrpc: '2.0',86id: 1,87method: 'resourceList',88params: { uri: URI.file('/workspace').toString() },89});9091transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [] } });92assert.deepStrictEqual(await resultPromise, { entries: [] });9394transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [{ name: 'late', type: 'file' }] } });95assert.strictEqual(transport.sentMessages.length, 1);96});9798test('preserves JSON-RPC error code and data', async () => {99const { client, transport } = createClient();100const resultPromise = client.resourceRead(URI.file('/missing'));101const data = { uri: URI.file('/missing').toString() };102103transport.fireMessage({ jsonrpc: '2.0', id: 1, error: { code: AhpErrorCodes.NotFound, message: 'Missing resource', data } });104105await assertRemoteProtocolError(resultPromise, { code: AhpErrorCodes.NotFound, message: 'Missing resource', data });106});107108test('ignores response for unknown request id', () => {109const { transport } = createClient();110111transport.fireMessage({ jsonrpc: '2.0', id: 99, result: null });112113assert.strictEqual(transport.sentMessages.length, 0);114});115116test('rejects all pending requests on transport close', async () => {117const { client, transport } = createClient();118const first = client.resourceList(URI.file('/one'));119const second = client.resourceRead(URI.file('/two'));120let closeCount = 0;121disposables.add(client.onDidClose(() => closeCount++));122const firstRejected = assertRemoteProtocolError(first, { code: -32000, message: 'Connection closed: test.example:1234' });123const secondRejected = assertRemoteProtocolError(second, { code: -32000, message: 'Connection closed: test.example:1234' });124125transport.fireClose();126transport.fireClose();127128await firstRejected;129await secondRejected;130assert.strictEqual(closeCount, 1);131});132133test('rejects pending requests on dispose', async () => {134const { client } = createClient();135const resultPromise = client.resourceList(URI.file('/workspace'));136const rejected = assertRemoteProtocolError(resultPromise, { code: -32000, message: 'Connection disposed: test.example:1234' });137138client.dispose();139140await rejected;141});142143test('dispose rejection wins when transport emits close while disposing', async () => {144const transport = disposables.add(new CloseOnDisposeProtocolTransport());145const { client } = createClient(transport);146const resultPromise = client.resourceList(URI.file('/workspace'));147const rejected = assertRemoteProtocolError(resultPromise, { code: -32000, message: 'Connection disposed: test.example:1234' });148149client.dispose();150151await rejected;152});153154test('late response after close does not complete rejected request', async () => {155const { client, transport } = createClient();156const resultPromise = client.resourceList(URI.file('/workspace'));157const rejected = assertRemoteProtocolError(resultPromise, { code: -32000, message: 'Connection closed: test.example:1234' });158159transport.fireClose();160transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [] } });161162await rejected;163});164165test('rejects requests started after transport close', async () => {166const { client, transport } = createClient();167168transport.fireClose();169170await assertRemoteProtocolError(client.resourceList(URI.file('/workspace')), { code: -32000, message: 'Connection closed: test.example:1234' });171assert.strictEqual(transport.sentMessages.length, 0);172});173174test('rejects requests started after dispose', async () => {175const { client, transport } = createClient();176177client.dispose();178179await assertRemoteProtocolError(client.resourceList(URI.file('/workspace')), { code: -32000, message: 'Connection disposed: test.example:1234' });180assert.strictEqual(transport.sentMessages.length, 0);181});182183test('rejects connect when transport closes before connect completes', async () => {184const transport = disposables.add(new TestClientProtocolTransport());185const { client } = createClient(transport);186const rejected = assertRemoteProtocolError(client.connect(), { code: -32000, message: 'Connection closed: test.example:1234' });187188transport.fireClose();189transport.connectDeferred.complete();190191await rejected;192assert.strictEqual(transport.sentMessages.length, 0);193});194195test('rejects connect when disposed before transport connect completes', async () => {196const transport = disposables.add(new TestClientProtocolTransport());197const { client } = createClient(transport);198const rejected = assertRemoteProtocolError(client.connect(), { code: -32000, message: 'Connection disposed: test.example:1234' });199200client.dispose();201202await rejected;203assert.strictEqual(transport.sentMessages.length, 0);204});205206test('sends shutdown as a JSON-RPC request shape', async () => {207const { client, transport } = createClient();208const resultPromise = client.shutdown();209210assert.deepStrictEqual(transport.sentMessages[0], {211jsonrpc: '2.0',212id: 1,213method: 'shutdown',214params: undefined,215});216217transport.fireMessage({ jsonrpc: '2.0', id: 1, result: null });218await resultPromise;219});220221test('rejects shutdown with structured JSON-RPC error', async () => {222const { client, transport } = createClient();223const resultPromise = client.shutdown();224225transport.fireMessage({ jsonrpc: '2.0', id: 1, error: { code: AhpErrorCodes.TurnInProgress, message: 'Turn in progress' } });226227await assertRemoteProtocolError(resultPromise, { code: AhpErrorCodes.TurnInProgress, message: 'Turn in progress' });228});229});230231232