Files
ccs/tests/unit/cursor/cursor-protobuf.test.ts
T

658 lines
21 KiB
TypeScript

/**
* Cursor Protobuf Module Unit Tests
* Tests encoder, decoder, translator, and executor components
*/
import { describe, it, expect } from 'bun:test';
import {
encodeVarint,
encodeField,
wrapConnectRPCFrame,
concatArrays,
} from '../../../src/cursor/cursor-protobuf-encoder';
import {
decodeVarint,
decodeField,
parseConnectRPCFrame,
} from '../../../src/cursor/cursor-protobuf-decoder';
import { buildCursorRequest } from '../../../src/cursor/cursor-translator';
import { generateCursorBody } from '../../../src/cursor/cursor-protobuf';
import { CursorExecutor } from '../../../src/cursor/cursor-executor';
import { WIRE_TYPE, FIELD } from '../../../src/cursor/cursor-protobuf-schema';
describe('Protobuf Encoding/Decoding', () => {
describe('encodeVarint / decodeVarint round-trip', () => {
it('should encode and decode 0', () => {
const encoded = encodeVarint(0);
const [decoded, offset] = decodeVarint(encoded, 0);
expect(decoded).toBe(0);
expect(offset).toBe(1);
});
it('should encode and decode 1', () => {
const encoded = encodeVarint(1);
const [decoded, offset] = decodeVarint(encoded, 0);
expect(decoded).toBe(1);
expect(offset).toBe(1);
});
it('should encode and decode 127', () => {
const encoded = encodeVarint(127);
const [decoded, offset] = decodeVarint(encoded, 0);
expect(decoded).toBe(127);
expect(offset).toBe(1);
});
it('should encode and decode 128', () => {
const encoded = encodeVarint(128);
const [decoded, offset] = decodeVarint(encoded, 0);
expect(decoded).toBe(128);
expect(offset).toBe(2);
});
it('should encode and decode 16383', () => {
const encoded = encodeVarint(16383);
const [decoded, offset] = decodeVarint(encoded, 0);
expect(decoded).toBe(16383);
expect(offset).toBe(2);
});
it('should encode and decode 0xFFFFFFFF', () => {
const encoded = encodeVarint(0xffffffff);
const [decoded, offset] = decodeVarint(encoded, 0);
expect(decoded).toBe(0xffffffff);
expect(offset).toBe(5);
});
});
describe('encodeField / decodeField round-trip', () => {
it('should encode and decode VARINT field', () => {
const fieldNum = 5;
const value = 42;
const encoded = encodeField(fieldNum, WIRE_TYPE.VARINT, value);
const [decodedFieldNum, wireType, decodedValue, offset] = decodeField(encoded, 0);
expect(decodedFieldNum).toBe(fieldNum);
expect(wireType).toBe(WIRE_TYPE.VARINT);
expect(decodedValue).toBe(value);
expect(offset).toBe(encoded.length);
});
it('should encode and decode LEN field with string', () => {
const fieldNum = 10;
const value = 'Hello, World!';
const encoded = encodeField(fieldNum, WIRE_TYPE.LEN, value);
const [decodedFieldNum, wireType, decodedValue, offset] = decodeField(encoded, 0);
expect(decodedFieldNum).toBe(fieldNum);
expect(wireType).toBe(WIRE_TYPE.LEN);
expect(new TextDecoder().decode(decodedValue as Uint8Array)).toBe(value);
expect(offset).toBe(encoded.length);
});
it('should encode and decode LEN field with binary data', () => {
const fieldNum = 15;
const value = new Uint8Array([1, 2, 3, 4, 5]);
const encoded = encodeField(fieldNum, WIRE_TYPE.LEN, value);
const [decodedFieldNum, wireType, decodedValue, offset] = decodeField(encoded, 0);
expect(decodedFieldNum).toBe(fieldNum);
expect(wireType).toBe(WIRE_TYPE.LEN);
expect(decodedValue).toEqual(value);
expect(offset).toBe(encoded.length);
});
});
describe('wrapConnectRPCFrame / parseConnectRPCFrame round-trip', () => {
it('should wrap and parse uncompressed frame', () => {
const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const frame = wrapConnectRPCFrame(payload, false);
const parsed = parseConnectRPCFrame(Buffer.from(frame));
expect(parsed).not.toBeNull();
expect(parsed!.flags).toBe(0x00);
expect(parsed!.length).toBe(payload.length);
expect(parsed!.payload).toEqual(payload);
expect(parsed!.consumed).toBe(5 + payload.length);
});
it('should wrap and parse compressed frame', () => {
const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const frame = wrapConnectRPCFrame(payload, true);
const parsed = parseConnectRPCFrame(Buffer.from(frame));
expect(parsed).not.toBeNull();
expect(parsed!.flags).toBe(0x01); // GZIP flag
expect(parsed!.payload).toEqual(payload); // Should be decompressed
});
it('should handle incomplete frame', () => {
const partial = new Uint8Array([0x00, 0x00, 0x00]); // Only 3 bytes
const parsed = parseConnectRPCFrame(Buffer.from(partial));
expect(parsed).toBeNull();
});
});
describe('concatArrays', () => {
it('should concatenate multiple arrays', () => {
const arr1 = new Uint8Array([1, 2, 3]);
const arr2 = new Uint8Array([4, 5]);
const arr3 = new Uint8Array([6, 7, 8, 9]);
const result = concatArrays(arr1, arr2, arr3);
expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
});
it('should handle empty arrays', () => {
const arr1 = new Uint8Array([1, 2]);
const arr2 = new Uint8Array([]);
const arr3 = new Uint8Array([3, 4]);
const result = concatArrays(arr1, arr2, arr3);
expect(result).toEqual(new Uint8Array([1, 2, 3, 4]));
});
});
});
describe('Message Translation', () => {
describe('buildCursorRequest', () => {
it('should convert system message to user with prefix', () => {
const result = buildCursorRequest(
'gpt-4',
{
messages: [{ role: 'system', content: 'You are a helpful assistant.' }],
},
false,
{}
);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content).toContain('[System Instructions]');
expect(result.messages[0].content).toContain('You are a helpful assistant.');
});
it('should keep user and assistant messages', () => {
const result = buildCursorRequest(
'gpt-4',
{
messages: [
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' },
],
},
false,
{}
);
expect(result.messages).toHaveLength(2);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content).toBe('Hello');
expect(result.messages[1].role).toBe('assistant');
expect(result.messages[1].content).toBe('Hi there!');
});
it('should handle assistant messages with tool_calls', () => {
const result = buildCursorRequest(
'gpt-4',
{
messages: [
{
role: 'assistant',
content: '',
tool_calls: [
{
id: 'call_123',
type: 'function',
function: { name: 'get_weather', arguments: '{"city":"NYC"}' },
},
],
},
],
},
false,
{}
);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('assistant');
expect(result.messages[0].tool_calls).toHaveLength(1);
expect(result.messages[0].tool_calls![0].id).toBe('call_123');
expect(result.messages[0].tool_calls![0].function.name).toBe('get_weather');
});
it('should accumulate tool results', () => {
const result = buildCursorRequest(
'gpt-4',
{
messages: [
{
role: 'assistant',
content: '',
tool_calls: [
{
id: 'call_123',
type: 'function',
function: { name: 'get_weather', arguments: '{"city":"NYC"}' },
},
],
},
{
role: 'tool',
content: '{"temperature": 72}',
name: 'get_weather',
tool_call_id: 'call_123',
},
{ role: 'user', content: 'What is the weather?' },
],
},
false,
{}
);
expect(result.messages).toHaveLength(2);
// Tool result should be attached to next message
expect(result.messages[1].tool_results).toBeDefined();
expect(result.messages[1].tool_results).toHaveLength(1);
expect(result.messages[1].tool_results![0].tool_call_id).toBe('call_123');
});
it('should handle array content format', () => {
const result = buildCursorRequest(
'gpt-4',
{
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Hello' },
{ type: 'text', text: ' World' },
],
},
],
},
false,
{}
);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].content).toBe('Hello World');
});
it('should handle system message with array content format', () => {
const result = buildCursorRequest(
'gpt-4',
{
messages: [
{
role: 'system',
content: [
{ type: 'text', text: 'System instruction part 1' },
{ type: 'text', text: ' part 2' },
],
},
],
},
false,
{}
);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content).toBe('[System Instructions]\nSystem instruction part 1 part 2');
});
});
});
describe('Request Encoding', () => {
describe('generateCursorBody', () => {
it('should encode basic text message', () => {
const result = generateCursorBody([{ role: 'user', content: 'Hello' }], 'gpt-4', [], null);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBeGreaterThan(0);
});
it('should encode message with tools', () => {
const tools = [
{
type: 'function' as const,
function: {
name: 'get_weather',
description: 'Get weather data',
parameters: {
type: 'object',
properties: {
city: { type: 'string' },
},
required: ['city'],
},
},
},
];
const result = generateCursorBody([{ role: 'user', content: 'What is the weather?' }], 'gpt-4', tools, null);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBeGreaterThan(0);
});
});
describe('Edge cases', () => {
it('should handle malformed frame gracefully', () => {
const executor = new CursorExecutor();
// Incomplete frame header (only 3 bytes instead of 5)
const incompleteFrame = Buffer.from([0x00, 0x00, 0x00]);
const result = executor.transformProtobufToJSON(incompleteFrame, 'gpt-4', {
messages: [],
});
// Should return valid response even with malformed input
expect(result.status).toBe(200);
});
it('should handle truncated payload', () => {
const executor = new CursorExecutor();
// Frame header says payload is 100 bytes but only 5 bytes follow
const truncatedFrame = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05]);
const result = executor.transformProtobufToJSON(truncatedFrame, 'gpt-4', {
messages: [],
});
// Should handle gracefully
expect(result.status).toBe(200);
});
it('should handle multi-frame buffer', () => {
const executor = new CursorExecutor();
// Create two simple frames
const frame1 = wrapConnectRPCFrame(
encodeField(FIELD.RESPONSE_TEXT, WIRE_TYPE.LEN, 'Frame 1'),
false
);
const frame2 = wrapConnectRPCFrame(
encodeField(FIELD.RESPONSE_TEXT, WIRE_TYPE.LEN, ' Frame 2'),
false
);
// Concatenate them
const multiFrame = Buffer.concat([Buffer.from(frame1), Buffer.from(frame2)]);
const result = executor.transformProtobufToJSON(multiFrame, 'gpt-4', {
messages: [],
});
expect(result.status).toBe(200);
});
});
});
describe('CursorExecutor', () => {
const executor = new CursorExecutor();
describe('generateChecksum', () => {
it('should generate valid checksum format', () => {
const machineId = 'test-machine-id';
const checksum = executor.generateChecksum(machineId);
// Should end with machine ID
expect(checksum.endsWith(machineId)).toBe(true);
// Should have base64url-like prefix (8 chars from 6 bytes)
const prefix = checksum.slice(0, -machineId.length);
expect(prefix.length).toBe(8);
expect(/^[A-Za-z0-9_-]+$/.test(prefix)).toBe(true);
});
it('should generate valid checksums at different call times', async () => {
const machineId = 'test-machine-id';
const checksum1 = executor.generateChecksum(machineId);
// Wait to ensure timestamp may change (though timestamp granularity is ~16 min)
await new Promise((resolve) => setTimeout(resolve, 10));
const checksum2 = executor.generateChecksum(machineId);
// Verify both checksums are valid (may be same due to timestamp granularity)
expect(checksum1.endsWith(machineId)).toBe(true);
expect(checksum2.endsWith(machineId)).toBe(true);
});
});
describe('buildHeaders', () => {
it('should generate all required headers', () => {
const credentials = {
accessToken: 'test-token',
machineId: 'test-machine-id',
};
const headers = executor.buildHeaders(credentials);
expect(headers).toHaveProperty('authorization');
expect(headers.authorization).toContain('Bearer');
expect(headers).toHaveProperty('connect-accept-encoding', 'gzip');
expect(headers).toHaveProperty('connect-protocol-version', '1');
expect(headers).toHaveProperty('content-type', 'application/connect+proto');
expect(headers).toHaveProperty('user-agent', 'connect-es/1.6.1');
expect(headers).toHaveProperty('x-cursor-checksum');
expect(headers).toHaveProperty('x-cursor-client-version', '2.3.41');
expect(headers).toHaveProperty('x-cursor-client-type', 'ide');
expect(headers).toHaveProperty('x-ghost-mode', 'true');
});
it('should handle token with :: delimiter', () => {
const credentials = {
accessToken: 'prefix::actual-token',
machineId: 'test-machine-id',
};
const headers = executor.buildHeaders(credentials);
expect(headers.authorization).toBe('Bearer actual-token');
});
it('should respect ghostMode flag', () => {
const credentialsGhost = {
accessToken: 'test-token',
machineId: 'test-machine-id',
ghostMode: true,
};
const credentialsNoGhost = {
accessToken: 'test-token',
machineId: 'test-machine-id',
ghostMode: false,
};
const headersGhost = executor.buildHeaders(credentialsGhost);
const headersNoGhost = executor.buildHeaders(credentialsNoGhost);
expect(headersGhost['x-ghost-mode']).toBe('true');
expect(headersNoGhost['x-ghost-mode']).toBe('false');
});
it('should throw error if machineId missing', () => {
const credentials = {
accessToken: 'test-token',
machineId: '',
};
expect(() => executor.buildHeaders(credentials)).toThrow('Machine ID is required');
});
});
describe('buildUrl', () => {
it('should return correct API endpoint', () => {
const url = executor.buildUrl();
expect(url).toBe('https://api2.cursor.sh/aiserver.v1.AiService/StreamChat');
});
});
describe('transformProtobufToJSON', () => {
it('should handle basic text response', async () => {
// Create minimal protobuf response with text
const textContent = 'Hello, world!';
const responseField = encodeField(FIELD.RESPONSE_TEXT, WIRE_TYPE.LEN, textContent);
const responseMsg = encodeField(FIELD.RESPONSE, WIRE_TYPE.LEN, responseField);
const frame = wrapConnectRPCFrame(responseMsg, false);
const result = executor.transformProtobufToJSON(Buffer.from(frame), 'gpt-4', {
messages: [],
});
expect(result.status).toBe(200);
const bodyText = await result.text();
const body = JSON.parse(bodyText);
expect(body.choices[0].message.content).toBe(textContent);
expect(body.choices[0].finish_reason).toBe('stop');
});
it('should handle JSON error response', async () => {
const errorJson = JSON.stringify({
error: {
code: 'resource_exhausted',
message: 'Rate limit exceeded',
},
});
const frame = wrapConnectRPCFrame(new TextEncoder().encode(errorJson), false);
const result = executor.transformProtobufToJSON(Buffer.from(frame), 'gpt-4', {
messages: [],
});
expect(result.status).toBe(429);
const bodyText = await result.text();
const body = JSON.parse(bodyText);
expect(body.error.type).toBe('rate_limit_error');
});
});
describe('transformProtobufToSSE', () => {
it('should output SSE format', async () => {
// Create minimal protobuf response with text
const textContent = 'Hello';
const responseField = encodeField(FIELD.RESPONSE_TEXT, WIRE_TYPE.LEN, textContent);
const responseMsg = encodeField(FIELD.RESPONSE, WIRE_TYPE.LEN, responseField);
const frame = wrapConnectRPCFrame(responseMsg, false);
const result = executor.transformProtobufToSSE(Buffer.from(frame), 'gpt-4', {
messages: [],
});
expect(result.status).toBe(200);
expect(result.headers.get('content-type')).toBe('text/event-stream');
const bodyText = await result.text();
expect(bodyText).toContain('data: ');
expect(bodyText).toContain('data: [DONE]');
expect(bodyText).toContain(textContent);
});
it('should handle JSON error response', async () => {
const errorJson = JSON.stringify({
error: {
code: 'resource_exhausted',
message: 'Rate limit exceeded',
},
});
const frame = wrapConnectRPCFrame(new TextEncoder().encode(errorJson), false);
const result = executor.transformProtobufToSSE(Buffer.from(frame), 'gpt-4', {
messages: [],
});
expect(result.status).toBe(429);
const bodyText = await result.text();
const body = JSON.parse(bodyText);
expect(body.error.type).toBe('rate_limit_error');
});
});
describe('decompressPayload error handling', () => {
it('should return empty buffer on decompression failure', () => {
// Create invalid gzip data
const invalidGzip = Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0xff, 0xff]);
const frame = new Uint8Array(5 + invalidGzip.length);
frame[0] = 0x01; // GZIP flag
frame[1] = 0;
frame[2] = 0;
frame[3] = 0;
frame[4] = invalidGzip.length;
frame.set(invalidGzip, 5);
const result = executor.transformProtobufToJSON(Buffer.from(frame), 'gpt-4', {
messages: [],
});
// Should handle gracefully and return valid response
expect(result.status).toBe(200);
});
});
describe('error handling', () => {
it('should return empty buffer on decompression failure', () => {
const executor = new CursorExecutor();
// Invalid compressed payload (not actually gzipped)
const invalidGzipPayload = new Uint8Array([1, 2, 3, 4, 5]);
const flags = 0x01; // GZIP flag
// Wrap with ConnectRPC frame header (flags + length)
const length = invalidGzipPayload.length;
const frame = new Uint8Array(5 + length);
frame[0] = flags;
frame[1] = (length >> 24) & 0xff;
frame[2] = (length >> 16) & 0xff;
frame[3] = (length >> 8) & 0xff;
frame[4] = length & 0xff;
frame.set(invalidGzipPayload, 5);
const buffer = Buffer.from(frame);
// Should not crash - decompression failure returns empty buffer
const result = executor.transformProtobufToJSON(buffer, 'test-model', {
messages: [],
stream: false,
});
expect(result.status).toBe(200);
});
it('should log unknown message roles in debug mode', () => {
const originalDebug = process.env.CCS_DEBUG;
process.env.CCS_DEBUG = '1';
const consoleSpy: string[] = [];
const originalError = console.error;
console.error = (...args: unknown[]) => {
const msg = args.map((a) => String(a)).join(' ');
consoleSpy.push(msg);
};
try {
const messages = [
{
role: 'unknown_role' as 'user', // Type assertion to bypass TS
content: 'test',
},
];
// buildCursorRequest expects (model, body, stream, credentials)
buildCursorRequest('test-model', { messages }, false, { machineId: '12345', accessToken: 'test' });
// Should have logged warning
const hasWarning = consoleSpy.some((log) => log.includes('Unknown message role'));
expect(hasWarning).toBe(true);
} finally {
console.error = originalError;
process.env.CCS_DEBUG = originalDebug;
}
});
});
});