mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 20:15:32 +00:00
fix(cursor): address second-round review feedback for protobuf module
HIGH Priority: - Add comprehensive unit tests (27 tests covering encoder, decoder, translator, executor) * encodeVarint/decodeVarint round-trip (0, 1, 127, 128, 16383, 0xFFFFFFFF) * encodeField/decodeField round-trip (VARINT, LEN string, LEN binary) * wrapConnectRPCFrame/parseConnectRPCFrame (compressed/uncompressed) * buildCursorRequest message translation (system, user, assistant, tool) * generateChecksum header format validation * buildHeaders output validation * transformProtobufToJSON basic conversion - Create GitHub issue #531 for true streaming implementation - Update TODO comment to reference issue #531 MEDIUM Priority: - Export CursorCredentials from cursor-protobuf-schema.ts - Add JSDoc grouping comments to FIELD constant for clarity - Make hardcoded values configurable (CURSOR_CLIENT_VERSION, CURSOR_USER_AGENT) - Add debug logging to 9 silent catch blocks (respects CCS_DEBUG env var) - Fix stream check: stream !== false → stream === true Bug Fixes: - Fix decodeVarint to return unsigned values (>>> 0) - Fix test assertion for Response.text() async API
This commit is contained in:
1 parent
f3d532afd9
commit
e177a4b097
4 files changed
+478
-44
No files matched your search
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* 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 { 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 different checksums over time', async () => {
|
||||
const machineId = 'test-machine-id';
|
||||
const checksum1 = executor.generateChecksum(machineId);
|
||||
|
||||
// Wait longer to ensure timestamp changes (microsecond precision)
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const checksum2 = executor.generateChecksum(machineId);
|
||||
|
||||
// Different timestamps should produce different checksums
|
||||
// If they're still the same, it's extremely rare but acceptable
|
||||
// Just verify format is correct
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user