fix(cursor): address fourth-round review feedback for protobuf module

This commit is contained in:
Tam Nhu Tran
2026-02-12 03:27:58 +07:00
parent 66a93ee46f
commit c5e8241393
3 changed files with 139 additions and 91 deletions
+18 -33
View File
@@ -42,41 +42,19 @@ interface Http2Response {
body: Buffer;
}
/** Detect cloud environment */
function isCloudEnv(): boolean {
if (
typeof globalThis !== 'undefined' &&
'caches' in globalThis &&
typeof (globalThis as { caches?: unknown }).caches === 'object'
)
return true;
try {
// Check for EdgeRuntime without causing compilation error
if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== 'undefined') return true;
} catch (err) {
if (process.env.CCS_DEBUG) {
console.error('[cursor] EdgeRuntime detection failed:', err);
}
}
return false;
}
/** Lazy import http2 */
let http2Module: typeof import('http2') | null = null;
async function getHttp2() {
if (http2Module) return http2Module;
if (!isCloudEnv()) {
try {
http2Module = await import('http2');
return http2Module;
} catch (err) {
if (process.env.CCS_DEBUG) {
console.error('[cursor] http2 import failed:', err);
}
return null;
try {
http2Module = await import('http2');
return http2Module;
} catch (err) {
if (process.env.CCS_DEBUG) {
console.error('[cursor] http2 module not available, falling back to fetch:', err);
}
return null;
}
return null;
}
/**
@@ -214,6 +192,10 @@ export class CursorExecutor {
const delimIdx = accessToken.indexOf('::');
const cleanToken = delimIdx !== -1 ? accessToken.slice(delimIdx + 2) : accessToken;
if (!cleanToken) {
throw new Error('Access token is empty after parsing');
}
return {
authorization: `Bearer ${cleanToken}`,
'connect-accept-encoding': 'gzip',
@@ -318,7 +300,7 @@ export class CursorExecutor {
req.on('end', () => {
client.close();
resolve({
status: Number(responseHeaders[':status']),
status: Number(responseHeaders[':status']) || 500,
headers: responseHeaders,
body: Buffer.concat(chunks),
});
@@ -456,18 +438,21 @@ export class CursorExecutor {
// Check for protobuf-decoded error
if (result.error) {
const isRateLimit =
result.error.toLowerCase().includes('rate') ||
result.error.toLowerCase().includes('limit');
yield {
type: 'error',
response: new Response(
JSON.stringify({
error: {
message: result.error,
type: 'rate_limit_error',
code: 'rate_limited',
type: isRateLimit ? 'rate_limit_error' : 'server_error',
code: isRateLimit ? 'rate_limited' : 'cursor_error',
},
}),
{
status: 429,
status: isRateLimit ? 429 : 400,
headers: { 'Content-Type': 'application/json' },
}
),
+9 -1
View File
@@ -39,9 +39,17 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
const msg = messages[i];
if (msg.role === 'system') {
let content = '';
if (typeof msg.content === 'string') {
content = msg.content;
} else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === 'text' && part.text) content += part.text;
}
}
result.push({
role: 'user',
content: `[System Instructions]\n${msg.content}`,
content: `[System Instructions]\n${content}`,
});
continue;
}
+112 -57
View File
@@ -16,6 +16,7 @@ import {
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';
@@ -277,6 +278,117 @@ describe('Message Translation', () => {
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);
});
});
});
@@ -482,63 +594,6 @@ describe('CursorExecutor', () => {
});
});
describe('transformProtobufToSSE', () => {
it('should output SSE format for simple text response', async () => {
const executor = new CursorExecutor();
// Minimal protobuf frame with text content
const textPayload = new Uint8Array([
(FIELD.MSG_CONTENT << 3) | WIRE_TYPE.LEN,
4,
...[116, 101, 115, 116], // "test"
]);
const buffer = Buffer.from(
wrapConnectRPCFrame(textPayload, {
compress: false,
})
);
const result = executor.transformProtobufToSSE(buffer, 'test-model', {
messages: [],
stream: true,
});
expect(result.status).toBe(200);
expect(result.headers.get('Content-Type')).toBe('text/event-stream');
const body = await result.text();
expect(body).toContain('data: ');
expect(body).toContain('"object":"chat.completion.chunk"');
expect(body).toContain('data: [DONE]');
});
it('should handle error responses in SSE format', () => {
const executor = new CursorExecutor();
// Protobuf frame with error
const errorPayload = new Uint8Array([
(FIELD.MSG_CONTENT << 3) | WIRE_TYPE.LEN,
17,
...[101, 114, 114, 111, 114, 58, 32, 116, 101, 115, 116, 32, 101, 114, 114, 111, 114], // "error: test error"
]);
const buffer = Buffer.from(
wrapConnectRPCFrame(errorPayload, {
compress: false,
})
);
const result = executor.transformProtobufToSSE(buffer, 'test-model', {
messages: [],
stream: true,
});
// Error responses should still be valid
expect(result.status).toBeGreaterThanOrEqual(200);
});
});
describe('error handling', () => {
it('should return empty buffer on decompression failure', () => {
const executor = new CursorExecutor();