/** * 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, decodeMessage, 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'; import { StreamingFrameParser, decompressPayload } from '../../../src/cursor/cursor-stream-parser'; const MAX_TOOL_RESULT_CHARS = 12_000; function computeExpectedToolResultOmittedChars(textLength: number): number { if (textLength <= MAX_TOOL_RESULT_CHARS) { return 0; } let omittedChars = textLength - MAX_TOOL_RESULT_CHARS; while (true) { const suffix = `\n[truncated ${omittedChars} chars]`; const keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0); const nextOmittedChars = textLength - keepLength; if (nextOmittedChars === omittedChars) { return omittedChars; } omittedChars = nextOmittedChars; } } 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 flatten tool results into user tool_result blocks', () => { 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(3); expect(result.messages[1].role).toBe('user'); expect(result.messages[1].content).toContain(''); expect(result.messages[1].content).toContain('get_weather'); expect(result.messages[1].content).toContain('call_123'); expect(result.messages[2]).toEqual({ role: 'user', content: 'What is the weather?' }); }); it('should preserve consecutive tool messages as separate user turns', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_one', type: 'function', function: { name: 'search_docs', arguments: '{"q":"cursor"}' }, }, { id: 'call_two', type: 'function', function: { name: 'read_file', arguments: '{"path":"README.md"}' }, }, ], }, { role: 'tool', content: 'first result', tool_call_id: 'call_one', }, { role: 'tool', content: 'second result', tool_call_id: 'call_two', }, { role: 'user', content: 'Summarize both results.' }, ], }, false, {} ); expect(result.messages).toHaveLength(4); expect(result.messages[1]).toEqual({ role: 'user', content: expect.stringContaining('call_one'), }); expect(result.messages[2]).toEqual({ role: 'user', content: expect.stringContaining('call_two'), }); expect(result.messages[3]).toEqual({ role: 'user', content: 'Summarize both results.' }); }); it('should recover tool names for tool results without a name field', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_456', type: 'function', function: { name: 'search_docs', arguments: '{"q":"cursor"}' }, }, ], }, { role: 'tool', content: 'done', tool_call_id: 'call_456', }, ], }, false, {} ); expect(result.messages).toHaveLength(2); expect(result.messages[1].content).toContain('search_docs'); expect(result.messages[1].tool_results).toBeUndefined(); }); it('should flatten user content arrays that include tool_result blocks', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_789', type: 'function', function: { name: 'search_docs', arguments: '{"q":"cursor"}' }, }, ], }, { role: 'user', content: [ { type: 'text', text: 'Tool finished.' }, { type: 'tool_result', tool_use_id: 'call_789', content: { answer: 'Cursor integration ready' }, }, ], }, ], }, false, {} ); expect(result.messages).toHaveLength(2); expect(result.messages[1].role).toBe('user'); expect(result.messages[1].content).toContain('Tool finished.'); expect(result.messages[1].content).toContain('search_docs'); expect(result.messages[1].content).toContain('{"answer":"Cursor integration ready"}'); }); it('should preserve line breaks for multipart tool_result text content', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_lines', type: 'function', function: { name: 'read_file', arguments: '{"path":"notes.txt"}' }, }, ], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_lines', content: [ { type: 'text', text: 'first line' }, { type: 'text', text: 'second line' }, ], }, ], }, ], }, false, {} ); expect(result.messages).toHaveLength(2); expect(result.messages[1].content).toContain('first line\nsecond line'); }); it('should convert assistant tool_use content blocks into tool_calls', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: [ { type: 'text', text: 'Inspecting the workspace.' }, { type: 'tool_use', id: 'call_999', name: 'list_files', input: { path: '/tmp' }, }, ], }, ], }, false, {} ); expect(result.messages).toHaveLength(1); expect(result.messages[0].role).toBe('assistant'); expect(result.messages[0].content).toBe('Inspecting the workspace.'); expect(result.messages[0].tool_calls).toEqual([ { id: 'call_999', type: 'function', function: { name: 'list_files', arguments: '{"path":"/tmp"}' }, }, ]); }); it('should synthesize fallback ids for tool_use blocks that omit them', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: [ { type: 'tool_use', name: 'search_docs', input: { q: 'cursor' }, }, ], }, ], }, false, {} ); expect(result.messages).toHaveLength(1); expect(result.messages[0].tool_calls).toHaveLength(1); expect(result.messages[0].tool_calls![0].id).toBe('toolu_cursor_fallback_0_0'); }); it('should normalize invalid assistant tool call ids before emitting tool results', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_bad\nline two', type: 'function', function: { name: 'search_docs', arguments: '{"q":"cursor"}' }, }, ], }, { role: 'tool', content: 'done', tool_call_id: 'call_bad\nline two', }, ], }, false, {} ); expect(result.messages).toHaveLength(2); expect(result.messages[0].tool_calls![0].id).toBe('call_bad'); expect(result.messages[1].content).toContain('call_bad'); }); it('should dedupe tool calls when assistant messages contain both tool_calls and tool_use blocks', () => { const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: [ { type: 'tool_use', id: 'call_dupe', name: 'search_docs', input: { q: 'cursor' }, }, ], tool_calls: [ { id: 'call_dupe', type: 'function', function: { name: 'search_docs', arguments: '{"q":"cursor"}' }, }, ], }, ], }, false, {} ); expect(result.messages).toHaveLength(1); expect(result.messages[0].tool_calls).toEqual([ { id: 'call_dupe', type: 'function', function: { name: 'search_docs', arguments: '{"q":"cursor"}' }, }, ]); }); it('should truncate oversized tool result payloads', () => { const oversizedResult = '&'.repeat(12_050); const omittedChars = computeExpectedToolResultOmittedChars(oversizedResult.length); const preservedChars = oversizedResult.length - omittedChars; const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_big', type: 'function', function: { name: 'read_file', arguments: '{"path":"big.txt"}' }, }, ], }, { role: 'tool', content: oversizedResult, tool_call_id: 'call_big', }, ], }, false, {} ); expect(result.messages).toHaveLength(2); expect(result.messages[1].content).toContain('[truncated '); expect(result.messages[1].content).toContain(`[truncated ${omittedChars} chars]`); const resultMatch = result.messages[1].content.match(/([\s\S]*)<\/result>/); expect(resultMatch).not.toBeNull(); expect((resultMatch?.[1].match(/&/g) ?? []).length).toBe(preservedChars); }); it('should mark unserializable structured tool results explicitly', () => { const circular: Record = {}; circular.self = circular; const result = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_bad', type: 'function', function: { name: 'read_json', arguments: '{"path":"bad.json"}' }, }, ], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_bad', content: circular, }, ], }, ], }, false, {} ); expect(result.messages).toHaveLength(2); expect(result.messages[1].content).toContain('[unserializable content]'); }); it('should reject tool_result blocks without a valid tool_use_id', () => { expect(() => buildCursorRequest( 'gpt-4', { messages: [ { role: 'user', content: [ { type: 'tool_result', content: 'done', }, ], }, ], }, false, {} ) ).toThrow('messages[0].content[0] must include a valid tool result id'); }); it('should reject tool role messages without a valid tool_call_id', () => { expect(() => buildCursorRequest( 'gpt-4', { messages: [ { role: 'tool', content: 'done', }, ], }, false, {} ) ).toThrow('messages[0].tool_call_id must include a valid tool result id'); }); 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 a raw top-level request protobuf for basic text messages', () => { const result = generateCursorBody([{ role: 'user', content: 'Hello' }], 'gpt-4', [], null); const topLevel = decodeMessage(result); const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array; const chatRequest = decodeMessage(requestPayload); const encodedMessages = (chatRequest.get(FIELD.Chat.MESSAGES) || []).map((entry) => decodeMessage(entry.value as Uint8Array) ); const decoder = new TextDecoder(); expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toBeGreaterThan(0); expect(Array.from(result.slice(0, 5))).not.toEqual([0, 0, 0, 1, 107]); expect(topLevel.has(FIELD.Request.REQUEST)).toBe(true); expect(encodedMessages).toHaveLength(1); expect(decoder.decode(encodedMessages[0].get(FIELD.Message.CONTENT)?.[0]?.value as Uint8Array)).toBe( 'Hello' ); }); it('should encode message with tools into the raw request payload', () => { 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 ); const topLevel = decodeMessage(result); const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array; const chatRequest = decodeMessage(requestPayload); expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toBeGreaterThan(0); expect(topLevel.has(FIELD.Request.REQUEST)).toBe(true); expect((chatRequest.get(FIELD.Chat.MCP_TOOLS) || []).length).toBe(1); }); it('should preserve flattened tool_result blocks through protobuf encoding', () => { const translated = buildCursorRequest( 'gpt-4', { messages: [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_wire', type: 'function', function: { name: 'read_file', arguments: '{"path":"notes.txt"}' }, }, ], }, { role: 'tool', content: 'workspace snapshot', tool_call_id: 'call_wire', }, ], }, false, {} ); const body = generateCursorBody(translated.messages, 'gpt-4', [], null); const topLevel = decodeMessage(body); const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array; const chatRequest = decodeMessage(requestPayload); const encodedMessages = (chatRequest.get(FIELD.Chat.MESSAGES) || []).map((entry) => decodeMessage(entry.value as Uint8Array) ); const decoder = new TextDecoder(); const contents = encodedMessages.map((message) => decoder.decode(message.get(FIELD.Message.CONTENT)?.[0]?.value as Uint8Array) ); expect(contents.some((content) => content.includes(''))).toBe(true); expect(contents.some((content) => content.includes('read_file'))).toBe( true ); expect( contents.some((content) => content.includes('call_wire')) ).toBe(true); }); }); describe('Edge cases', () => { it('should reject malformed frame headers', async () => { 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: [], }); expect(result.status).toBe(502); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('server_error'); expect(body.error.message).toContain('Truncated Cursor ConnectRPC frame'); }); it('should reject truncated payloads', async () => { 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: [], }); expect(result.status).toBe(502); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('server_error'); expect(body.error.message).toContain('Truncated Cursor ConnectRPC frame'); }); it('should handle multi-frame buffer', () => { const executor = new CursorExecutor(); // Build two valid response frames (top-level Response.RESPONSE wrapper). const frame1 = buildTextFrame('Frame 1'); const frame2 = buildTextFrame(' Frame 2'); // Concatenate them const multiFrame = Buffer.concat([frame1, 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 throw when token becomes empty after delimiter parsing', () => { const credentials = { accessToken: 'prefix::', machineId: 'test-machine-id', }; expect(() => executor.buildHeaders(credentials)).toThrow('Access token is empty'); }); it('should include normalized platform and timezone headers', () => { const credentials = { accessToken: 'test-token', machineId: 'test-machine-id', }; const headers = executor.buildHeaders(credentials); expect(['windows', 'macos', 'linux']).toContain(headers['x-cursor-client-os']); expect(['aarch64', 'x64']).toContain(headers['x-cursor-client-arch']); expect(typeof headers['x-cursor-timezone']).toBe('string'); expect(headers['x-cursor-timezone'].length).toBeGreaterThan(0); }); 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.ChatResponse.TEXT, WIRE_TYPE.LEN, textContent); const responseMsg = encodeField(FIELD.Response.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'); }); it('should map unavailable end-stream errors to 503', async () => { const unavailableFrame = buildFrame( new TextEncoder().encode( JSON.stringify({ error: { code: 'unavailable', message: 'upstream down', }, }) ), 0x02 ); const result = executor.transformProtobufToJSON(unavailableFrame, 'gpt-4', { messages: [], }); expect(result.status).toBe(503); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('api_error'); expect(body.error.message).toContain('upstream down'); }); it('should surface reasoning_content when thinking payload is present', async () => { const textContent = 'Final answer'; const thinkingContent = 'Internal reasoning trail'; const thinkingField = encodeField(FIELD.Thinking.TEXT, WIRE_TYPE.LEN, thinkingContent); const chatResponse = concatArrays( encodeField(FIELD.ChatResponse.TEXT, WIRE_TYPE.LEN, textContent), encodeField(FIELD.ChatResponse.THINKING, WIRE_TYPE.LEN, thinkingField) ); const responseMsg = encodeField(FIELD.Response.RESPONSE, WIRE_TYPE.LEN, chatResponse); 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].message.reasoning_content).toBe(thinkingContent); }); it('should merge fragmented tool call arguments and set tool_calls finish reason', async () => { const frame1 = buildToolCallFrame({ id: 'call_123', name: 'search_docs', args: '{"q":"hel', isLast: false, }); const frame2 = buildToolCallFrame({ id: 'call_123', name: 'search_docs', args: 'lo"}', isLast: true, }); const combined = Buffer.concat([frame1, frame2]); const result = executor.transformProtobufToJSON(combined, 'gpt-4', { messages: [], }); expect(result.status).toBe(200); const body = JSON.parse(await result.text()); expect(body.choices[0].finish_reason).toBe('tool_calls'); expect(body.choices[0].message.tool_calls[0].id).toBe('call_123'); expect(body.choices[0].message.tool_calls[0].function.name).toBe('search_docs'); expect(body.choices[0].message.tool_calls[0].function.arguments).toBe('{"q":"hello"}'); }); }); describe('transformProtobufToSSE', () => { it('should output SSE format', async () => { // Create minimal protobuf response with text const textContent = 'Hello'; const responseField = encodeField(FIELD.ChatResponse.TEXT, WIRE_TYPE.LEN, textContent); const responseMsg = encodeField(FIELD.Response.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'); }); it('should emit reasoning_content deltas for thinking payloads', async () => { const thinkingContent = 'Deliberate reasoning'; const thinkingField = encodeField(FIELD.Thinking.TEXT, WIRE_TYPE.LEN, thinkingContent); const chatResponse = encodeField(FIELD.ChatResponse.THINKING, WIRE_TYPE.LEN, thinkingField); const responseMsg = encodeField(FIELD.Response.RESPONSE, WIRE_TYPE.LEN, chatResponse); const frame = wrapConnectRPCFrame(responseMsg, false); const result = executor.transformProtobufToSSE(Buffer.from(frame), 'gpt-4', { messages: [], }); expect(result.status).toBe(200); const bodyText = await result.text(); expect(bodyText).toContain('reasoning_content'); expect(bodyText).toContain(thinkingContent); }); it('should emit tool call deltas and end with finish_reason tool_calls', async () => { const frame1 = buildToolCallFrame({ id: 'call_abc', name: 'search_docs', args: '{"q":"foo', isLast: false, }); const frame2 = buildToolCallFrame({ id: 'call_abc', name: 'search_docs', args: '"}', isLast: true, }); const combined = Buffer.concat([frame1, frame2]); const result = executor.transformProtobufToSSE(combined, 'gpt-4', { messages: [], }); expect(result.status).toBe(200); const bodyText = await result.text(); expect(bodyText).toContain('tool_calls'); expect(bodyText).toContain('search_docs'); expect(bodyText).toContain('"finish_reason":"tool_calls"'); }); }); describe('decompressPayload error handling', () => { it('returns an explicit executor error on decompression failure', async () => { // 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: [], }); expect(result.status).toBe(502); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('server_error'); expect(body.error.message).toContain('decompress'); }); }); describe('error handling', () => { it('returns an explicit error for invalid compressed payloads', async () => { 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); const result = executor.transformProtobufToJSON(buffer, 'test-model', { messages: [], stream: false, }); expect(result.status).toBe(502); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('server_error'); expect(body.error.message).toContain('decompress'); }); it('surfaces first-frame protocol errors in SSE mode without pretending success', async () => { const executor = new CursorExecutor(); const invalidGzipPayload = new Uint8Array([1, 2, 3, 4, 5]); const frame = buildFrame(invalidGzipPayload, 0x01); const result = executor.transformProtobufToSSE(frame, 'test-model', { messages: [], stream: true, }); expect(result.status).toBe(502); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('server_error'); expect(body.error.message).toContain('decompress'); }); it('emits an SSE error event without [DONE] when a later frame fails', async () => { const executor = new CursorExecutor(); const combined = Buffer.concat([ buildTextFrame('Before failure'), buildFrame(new Uint8Array([1, 2, 3, 4, 5]), 0x01), ]); const result = executor.transformProtobufToSSE(combined, 'test-model', { messages: [], stream: true, }); expect(result.status).toBe(200); const body = await result.text(); expect(body).toContain('Before failure'); expect(body).toContain('event: error'); expect(body).toContain('"type":"server_error"'); expect(body).not.toContain('data: [DONE]'); }); it('emits an SSE error event when trailing bytes leave a truncated frame', async () => { const executor = new CursorExecutor(); const combined = Buffer.concat([buildTextFrame('Partial success'), Buffer.from([0x00, 0x00, 0x00])]); const result = executor.transformProtobufToSSE(combined, 'test-model', { messages: [], stream: true, }); expect(result.status).toBe(200); const body = await result.text(); expect(body).toContain('Partial success'); expect(body).toContain('event: error'); expect(body).toContain('Truncated Cursor ConnectRPC frame'); expect(body).not.toContain('data: [DONE]'); }); it('returns an explicit error for unknown ConnectRPC frame flags', async () => { const executor = new CursorExecutor(); const result = executor.transformProtobufToJSON(buildFrame(new Uint8Array([0x01]), 0x04), 'gpt-4', { messages: [], }); expect(result.status).toBe(502); const body = JSON.parse(await result.text()); expect(body.error.type).toBe('server_error'); expect(body.error.message).toContain('0x04'); }); 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; } }); }); }); /** * Helper: build a ConnectRPC frame from raw payload bytes */ function buildFrame(payload: Uint8Array, flags = 0): Buffer { const header = Buffer.alloc(5); header[0] = flags; header.writeUInt32BE(payload.length, 1); return Buffer.concat([header, Buffer.from(payload)]); } /** * Helper: build a protobuf text response frame */ function buildTextFrame(text: string): Buffer { const responseField = encodeField(FIELD.ChatResponse.TEXT, WIRE_TYPE.LEN, text); const responseMsg = encodeField(FIELD.Response.RESPONSE, WIRE_TYPE.LEN, responseField); return buildFrame(responseMsg); } /** * Helper: build a protobuf thinking response frame */ function buildThinkingFrame(thinking: string): Buffer { const thinkingField = encodeField(FIELD.Thinking.TEXT, WIRE_TYPE.LEN, thinking); const responseField = encodeField(FIELD.ChatResponse.THINKING, WIRE_TYPE.LEN, thinkingField); const responseMsg = encodeField(FIELD.Response.RESPONSE, WIRE_TYPE.LEN, responseField); return buildFrame(responseMsg); } /** * Helper: build a protobuf tool call response frame */ function buildToolCallFrame(options: { id: string; name: string; args: string; isLast: boolean; }): Buffer { const toolCallPayload = concatArrays( encodeField(FIELD.ToolCall.ID, WIRE_TYPE.LEN, options.id), encodeField(FIELD.ToolCall.NAME, WIRE_TYPE.LEN, options.name), encodeField(FIELD.ToolCall.RAW_ARGS, WIRE_TYPE.LEN, options.args), encodeField(FIELD.ToolCall.IS_LAST, WIRE_TYPE.VARINT, options.isLast ? 1 : 0) ); const responseMsg = encodeField(FIELD.Response.TOOL_CALL, WIRE_TYPE.LEN, toolCallPayload); return buildFrame(responseMsg); } describe('StreamingFrameParser', () => { it('should parse a complete single frame', () => { const parser = new StreamingFrameParser(); const frame = buildTextFrame('Hello'); const results = parser.push(frame); expect(results.length).toBe(1); expect(results[0].type).toBe('text'); if (results[0].type === 'text') { expect(results[0].text).toBe('Hello'); } expect(parser.hasPartial()).toBe(false); }); it('should buffer partial frame header (< 5 bytes)', () => { const parser = new StreamingFrameParser(); const frame = buildTextFrame('Hi'); // Send only first 3 bytes (partial header) const results1 = parser.push(frame.slice(0, 3)); expect(results1.length).toBe(0); expect(parser.hasPartial()).toBe(true); // Send rest of the frame const results2 = parser.push(frame.slice(3)); expect(results2.length).toBe(1); expect(results2[0].type).toBe('text'); expect(parser.hasPartial()).toBe(false); }); it('should buffer partial frame payload', () => { const parser = new StreamingFrameParser(); const frame = buildTextFrame('Hello world'); // Send header + partial payload const splitPoint = 8; // past the 5-byte header but not full payload const results1 = parser.push(frame.slice(0, splitPoint)); expect(results1.length).toBe(0); expect(parser.hasPartial()).toBe(true); // Complete the frame const results2 = parser.push(frame.slice(splitPoint)); expect(results2.length).toBe(1); expect(results2[0].type).toBe('text'); if (results2[0].type === 'text') { expect(results2[0].text).toBe('Hello world'); } }); it('should parse multiple frames from single chunk', () => { const parser = new StreamingFrameParser(); const frame1 = buildTextFrame('First'); const frame2 = buildTextFrame('Second'); const combined = Buffer.concat([frame1, frame2]); const results = parser.push(combined); expect(results.length).toBe(2); expect(results[0].type).toBe('text'); expect(results[1].type).toBe('text'); if (results[0].type === 'text' && results[1].type === 'text') { expect(results[0].text).toBe('First'); expect(results[1].text).toBe('Second'); } }); it('should handle frame split across two chunks', () => { const parser = new StreamingFrameParser(); const frame1 = buildTextFrame('AAA'); const frame2 = buildTextFrame('BBB'); const combined = Buffer.concat([frame1, frame2]); // Split in the middle of frame2 const splitPoint = frame1.length + 3; const results1 = parser.push(combined.slice(0, splitPoint)); expect(results1.length).toBe(1); // frame1 complete expect(results1[0].type).toBe('text'); const results2 = parser.push(combined.slice(splitPoint)); expect(results2.length).toBe(1); // frame2 complete expect(results2[0].type).toBe('text'); if (results2[0].type === 'text') { expect(results2[0].text).toBe('BBB'); } }); it('should detect JSON error mid-stream', () => { const parser = new StreamingFrameParser(); // First: a valid text frame const textFrame = buildTextFrame('Before error'); const results1 = parser.push(textFrame); expect(results1.length).toBe(1); // Then: a JSON error frame const errorJson = JSON.stringify({ error: { code: 'resource_exhausted', message: 'Rate limit' }, }); const errorFrame = buildFrame(new TextEncoder().encode(errorJson)); const results2 = parser.push(errorFrame); expect(results2.length).toBe(1); expect(results2[0].type).toBe('error'); if (results2[0].type === 'error') { expect(results2[0].status).toBe(429); expect(results2[0].errorType).toBe('rate_limit_error'); } }); it('should surface end-stream JSON errors instead of treating them as gzip', () => { const parser = new StreamingFrameParser(); const endStreamError = buildFrame( new TextEncoder().encode( JSON.stringify({ error: { code: 'invalid_argument', message: 'parse binary: illegal tag: field no 0 wire type 0', }, }) ), 0x02 ); const results = parser.push(endStreamError); expect(results.length).toBe(1); expect(results[0].type).toBe('error'); if (results[0].type === 'error') { expect(results[0].status).toBe(400); expect(results[0].errorType).toBe('api_error'); expect(results[0].message).toContain('illegal tag'); } }); it('should map unavailable end-stream errors to 503 in the parser', () => { const parser = new StreamingFrameParser(); const endStreamError = buildFrame( new TextEncoder().encode( JSON.stringify({ error: { code: 'unavailable', message: 'upstream down', }, }) ), 0x02 ); const results = parser.push(endStreamError); expect(results.length).toBe(1); expect(results[0].type).toBe('error'); if (results[0].type === 'error') { expect(results[0].status).toBe(503); expect(results[0].errorType).toBe('api_error'); } }); it('should parse thinking frames', () => { const parser = new StreamingFrameParser(); const frame = buildThinkingFrame('Think step by step'); const results = parser.push(frame); expect(results.length).toBe(1); expect(results[0].type).toBe('thinking'); if (results[0].type === 'thinking') { expect(results[0].text).toBe('Think step by step'); } }); it('should parse tool call frames', () => { const parser = new StreamingFrameParser(); const frame = buildToolCallFrame({ id: 'call_parser', name: 'search_docs', args: '{"q":"docs"}', isLast: true, }); const results = parser.push(frame); expect(results.length).toBe(1); expect(results[0].type).toBe('toolCall'); if (results[0].type === 'toolCall') { expect(results[0].toolCall.id).toBe('call_parser'); expect(results[0].toolCall.function.name).toBe('search_docs'); expect(results[0].toolCall.function.arguments).toBe('{"q":"docs"}'); expect(results[0].toolCall.isLast).toBe(true); } }); it('should classify malformed protobuf payload as server error', () => { const parser = new StreamingFrameParser(); const malformedFrame = buildFrame(new Uint8Array([0xff, 0xff, 0xff])); const results = parser.push(malformedFrame); expect(results.length).toBe(1); expect(results[0].type).toBe('error'); if (results[0].type === 'error') { expect(results[0].status).toBe(502); expect(results[0].errorType).toBe('server_error'); expect(results[0].message).toContain('Malformed protobuf response'); } }); it('should reject invalid gzip-compressed frames explicitly', () => { const parser = new StreamingFrameParser(); const invalidCompressedFrame = buildFrame(new Uint8Array([1, 2, 3, 4, 5]), 0x01); const results = parser.push(invalidCompressedFrame); expect(results.length).toBe(1); expect(results[0].type).toBe('error'); if (results[0].type === 'error') { expect(results[0].status).toBe(502); expect(results[0].errorType).toBe('server_error'); expect(results[0].message).toContain('decompress'); } }); it('should reject unknown ConnectRPC frame flag bits', () => { const parser = new StreamingFrameParser(); const results = parser.push(buildFrame(new Uint8Array([0x01]), 0x04)); expect(results.length).toBe(1); expect(results[0].type).toBe('error'); if (results[0].type === 'error') { expect(results[0].status).toBe(502); expect(results[0].errorType).toBe('server_error'); expect(results[0].message).toContain('0x04'); } }); it('should report hasPartial() correctly', () => { const parser = new StreamingFrameParser(); expect(parser.hasPartial()).toBe(false); // Push partial data parser.push(Buffer.from([0x00, 0x00])); expect(parser.hasPartial()).toBe(true); // Push enough to complete the frame (empty payload) // header: flags=0, length=0 → 5 bytes total const emptyFrame = Buffer.alloc(5); emptyFrame[0] = 0; emptyFrame.writeUInt32BE(0, 1); const parser2 = new StreamingFrameParser(); parser2.push(emptyFrame); expect(parser2.hasPartial()).toBe(false); }); it('should surface truncated trailing bytes when the stream finishes', () => { const parser = new StreamingFrameParser(); parser.push(Buffer.from([0x00, 0x00, 0x00])); const results = parser.finish(); expect(results.length).toBe(1); expect(results[0].type).toBe('error'); if (results[0].type === 'error') { expect(results[0].status).toBe(502); expect(results[0].message).toContain('Truncated Cursor ConnectRPC frame'); } }); }); describe('decompressPayload', () => { it('should pass through uncompressed payload', () => { const payload = Buffer.from('raw protobuf data'); const result = decompressPayload(payload, 0x00); expect(result).toEqual(payload); }); it('should skip decompression for JSON error payloads', () => { const errorPayload = Buffer.from('{"error":"something went wrong"}'); const result = decompressPayload(errorPayload, 0x01); // GZIP flag but JSON error expect(result).toEqual(errorPayload); }); it('should throw on invalid gzip data', () => { const invalidGzip = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]); expect(() => decompressPayload(invalidGzip, 0x01)).toThrow( 'Failed to decompress Cursor ConnectRPC frame.' ); }); it('should throw on unknown ConnectRPC frame flags', () => { const payload = Buffer.from('payload'); expect(() => decompressPayload(payload, 0x04)).toThrow( 'Unsupported ConnectRPC frame flags: 0x04' ); }); it('should decompress valid gzip payload', () => { const zlib = require('zlib'); const original = Buffer.from('Hello compressed world'); const compressed = zlib.gzipSync(original); const result = decompressPayload(compressed, 0x01); expect(result.toString()).toBe('Hello compressed world'); }); it('should not decompress plain end-stream trailers and should handle compressed end-stream trailers', () => { const zlib = require('zlib'); const original = Buffer.from('test data'); const compressed = zlib.gzipSync(original); expect(decompressPayload(original, 0x02)).toEqual(original); expect(decompressPayload(compressed, 0x03).toString()).toBe('test data'); }); });