/** * 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; } }); }); });