@probelabs/probe
Version:
Node.js wrapper for the probe code search tool
311 lines (248 loc) • 8.79 kB
JavaScript
// Tests for ACP Connection
import { jest } from '@jest/globals';
import { EventEmitter } from 'events';
import { ACPConnection } from './connection.js';
import { ErrorCode, RequestMethod } from './types.js';
// Mock streams
class MockStream extends EventEmitter {
constructor() {
super();
this.encoding = null;
this.writtenData = [];
}
setEncoding(encoding) {
this.encoding = encoding;
}
write(data) {
this.writtenData.push(data);
return true;
}
}
describe('ACPConnection', () => {
let inputStream, outputStream, connection;
beforeEach(() => {
inputStream = new MockStream();
outputStream = new MockStream();
connection = new ACPConnection(inputStream, outputStream);
});
afterEach(() => {
// Clean up fake timers if they were used
if (jest.isMockFunction(setTimeout)) {
jest.useRealTimers();
}
connection.close();
});
describe('initialization', () => {
test('should initialize with correct default values', () => {
expect(connection.isConnected).toBe(false);
expect(connection.messageId).toBe(1);
expect(connection.buffer).toBe('');
expect(connection.pendingRequests.size).toBe(0);
});
test('should setup streams correctly', () => {
connection.start();
expect(connection.isConnected).toBe(true);
expect(inputStream.encoding).toBe('utf8');
});
});
describe('message handling', () => {
beforeEach(() => {
connection.start();
});
test('should parse valid JSON-RPC messages', (done) => {
const message = {
jsonrpc: '2.0',
method: RequestMethod.INITIALIZE,
id: 1,
params: { protocolVersion: '1' }
};
connection.on('request', (receivedMessage) => {
expect(receivedMessage).toEqual(message);
done();
});
inputStream.emit('data', JSON.stringify(message) + '\n');
});
test('should handle invalid JSON gracefully', () => {
inputStream.emit('data', 'invalid json\n');
// Should send parse error
expect(outputStream.writtenData).toHaveLength(1);
const response = JSON.parse(outputStream.writtenData[0]);
expect(response.error.code).toBe(ErrorCode.PARSE_ERROR);
});
test('should handle notifications', (done) => {
const notification = {
jsonrpc: '2.0',
method: 'someNotification',
params: { data: 'test' }
};
connection.on('notification', (receivedMessage) => {
expect(receivedMessage).toEqual(notification);
done();
});
inputStream.emit('data', JSON.stringify(notification) + '\n');
});
test('should validate message format', () => {
const invalidMessage = {
jsonrpc: '1.0', // Wrong version
method: 'test'
};
inputStream.emit('data', JSON.stringify(invalidMessage) + '\n');
// Should send invalid request error
expect(outputStream.writtenData).toHaveLength(1);
const response = JSON.parse(outputStream.writtenData[0]);
expect(response.error.code).toBe(ErrorCode.INVALID_REQUEST);
});
});
describe('sending messages', () => {
beforeEach(() => {
connection.start();
});
test('should send requests with auto-incrementing IDs', async () => {
const params = { test: 'data' };
// Don't await - we'll resolve it manually
const requestPromise = connection.sendRequest('testMethod', params);
// Check that request was sent
expect(outputStream.writtenData).toHaveLength(1);
const sentMessage = JSON.parse(outputStream.writtenData[0]);
expect(sentMessage).toEqual({
jsonrpc: '2.0',
method: 'testMethod',
params,
id: 1
});
// Simulate response
const response = {
jsonrpc: '2.0',
id: 1,
result: { success: true }
};
inputStream.emit('data', JSON.stringify(response) + '\n');
const result = await requestPromise;
expect(result).toEqual({ success: true });
});
test('should send notifications without ID', () => {
const params = { notification: 'data' };
connection.sendNotification('testNotification', params);
expect(outputStream.writtenData).toHaveLength(1);
const sentMessage = JSON.parse(outputStream.writtenData[0]);
expect(sentMessage).toEqual({
jsonrpc: '2.0',
method: 'testNotification',
params
});
expect(sentMessage.id).toBeUndefined();
});
test('should send responses', () => {
const result = { data: 'test' };
connection.sendResponse(123, result);
expect(outputStream.writtenData).toHaveLength(1);
const sentMessage = JSON.parse(outputStream.writtenData[0]);
expect(sentMessage).toEqual({
jsonrpc: '2.0',
id: 123,
result
});
});
test('should send errors', () => {
connection.sendError(456, ErrorCode.INTERNAL_ERROR, 'Test error', { detail: 'test' });
expect(outputStream.writtenData).toHaveLength(1);
const sentMessage = JSON.parse(outputStream.writtenData[0]);
expect(sentMessage).toEqual({
jsonrpc: '2.0',
id: 456,
error: {
code: ErrorCode.INTERNAL_ERROR,
message: 'Test error',
data: { detail: 'test' }
}
});
});
});
describe('request/response handling', () => {
beforeEach(() => {
connection.start();
});
test('should handle response errors', async () => {
const requestPromise = connection.sendRequest('testMethod');
const errorResponse = {
jsonrpc: '2.0',
id: 1,
error: {
code: ErrorCode.METHOD_NOT_FOUND,
message: 'Method not found'
}
};
inputStream.emit('data', JSON.stringify(errorResponse) + '\n');
await expect(requestPromise).rejects.toThrow('RPC Error -32601: Method not found');
});
test('should timeout requests', async () => {
// Set up fake timers before creating the promise
jest.useFakeTimers();
const requestPromise = connection.sendRequest('testMethod');
// Fast-forward time past the timeout
jest.advanceTimersByTime(30000);
await expect(requestPromise).rejects.toThrow('Request timeout');
jest.useRealTimers();
});
});
describe('connection management', () => {
test('should handle disconnection', (done) => {
connection.start();
connection.on('disconnect', () => {
expect(connection.isConnected).toBe(false);
done();
});
inputStream.emit('end');
});
test('should reject pending requests on close', async () => {
connection.start();
const requestPromise = connection.sendRequest('testMethod');
connection.close();
await expect(requestPromise).rejects.toThrow('Connection closed');
});
test('should handle stream errors', (done) => {
connection.start();
connection.on('error', (error) => {
expect(error.message).toBe('Test error');
done();
});
inputStream.emit('error', new Error('Test error'));
});
});
describe('buffering', () => {
beforeEach(() => {
connection.start();
});
test('should handle partial messages', (done) => {
const message = {
jsonrpc: '2.0',
method: 'test',
params: { data: 'test' }
};
connection.on('notification', (receivedMessage) => {
expect(receivedMessage).toEqual(message);
done();
});
const jsonString = JSON.stringify(message) + '\n';
// Send message in chunks
inputStream.emit('data', jsonString.substring(0, 10));
inputStream.emit('data', jsonString.substring(10, 20));
inputStream.emit('data', jsonString.substring(20));
});
test('should handle multiple messages in one chunk', () => {
const messages = [
{ jsonrpc: '2.0', method: 'test1' },
{ jsonrpc: '2.0', method: 'test2' }
];
const receivedMessages = [];
connection.on('notification', (message) => {
receivedMessages.push(message);
});
const chunk = messages.map(msg => JSON.stringify(msg)).join('\n') + '\n';
inputStream.emit('data', chunk);
expect(receivedMessages).toHaveLength(2);
expect(receivedMessages[0].method).toBe('test1');
expect(receivedMessages[1].method).toBe('test2');
});
});
});