mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
550 lines
18 KiB
TypeScript
550 lines
18 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { CursorExecutor } from "../../open-sse/executors/cursor.ts";
|
|
import {
|
|
decodeMessage,
|
|
encodeField,
|
|
parseConnectRPCFrame,
|
|
wrapConnectRPCFrame,
|
|
} from "../../open-sse/utils/cursorProtobuf.ts";
|
|
import {
|
|
buildCursorHeaders,
|
|
generateCursorChecksum,
|
|
generateHashed64Hex,
|
|
generateSessionId,
|
|
} from "../../open-sse/utils/cursorChecksum.ts";
|
|
import {
|
|
getCursorVersion,
|
|
resetCursorVersionCache,
|
|
} from "../../open-sse/utils/cursorVersionDetector.ts";
|
|
|
|
const LEN = 2;
|
|
const VARINT = 0;
|
|
const TOP_LEVEL_TOOL_CALL = 1;
|
|
const TOP_LEVEL_RESPONSE = 2;
|
|
const RESPONSE_TEXT = 1;
|
|
const TOOL_ID = 3;
|
|
const TOOL_NAME = 9;
|
|
const TOOL_RAW_ARGS = 10;
|
|
const TOOL_IS_LAST = 11;
|
|
|
|
function concatArrays(...arrays) {
|
|
const total = arrays.reduce((sum, array) => sum + array.length, 0);
|
|
const result = new Uint8Array(total);
|
|
let offset = 0;
|
|
|
|
for (const array of arrays) {
|
|
result.set(array, offset);
|
|
offset += array.length;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function buildTextFrame(text) {
|
|
return Buffer.from(
|
|
wrapConnectRPCFrame(
|
|
encodeField(TOP_LEVEL_RESPONSE, LEN, encodeField(RESPONSE_TEXT, LEN, text)),
|
|
false
|
|
)
|
|
);
|
|
}
|
|
|
|
function buildCompressedTextFrame(text) {
|
|
return Buffer.from(
|
|
wrapConnectRPCFrame(
|
|
encodeField(TOP_LEVEL_RESPONSE, LEN, encodeField(RESPONSE_TEXT, LEN, text)),
|
|
true
|
|
)
|
|
);
|
|
}
|
|
|
|
function buildToolCallFrame({ id, name, args, isLast }) {
|
|
return Buffer.from(
|
|
wrapConnectRPCFrame(
|
|
encodeField(
|
|
TOP_LEVEL_TOOL_CALL,
|
|
LEN,
|
|
concatArrays(
|
|
encodeField(TOOL_ID, LEN, id),
|
|
encodeField(TOOL_NAME, LEN, name),
|
|
encodeField(TOOL_RAW_ARGS, LEN, args),
|
|
encodeField(TOOL_IS_LAST, VARINT, isLast ? 1 : 0)
|
|
)
|
|
),
|
|
false
|
|
)
|
|
);
|
|
}
|
|
|
|
function buildJsonErrorFrame(error) {
|
|
return Buffer.from(wrapConnectRPCFrame(new TextEncoder().encode(JSON.stringify(error)), false));
|
|
}
|
|
|
|
test("CursorExecutor.buildUrl uses the configured Cursor endpoint", () => {
|
|
const executor = new CursorExecutor();
|
|
assert.equal(
|
|
executor.buildUrl(),
|
|
"https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
|
);
|
|
});
|
|
|
|
test("CursorExecutor.buildHeaders strips token prefixes and derives checksum/session headers", () => {
|
|
const executor = new CursorExecutor();
|
|
const originalDateNow = Date.now;
|
|
const originalDbPath = process.env.CURSOR_STATE_DB_PATH;
|
|
// Force fallback by pointing to a non-existent DB path
|
|
process.env.CURSOR_STATE_DB_PATH = "/nonexistent/cursor/state.vscdb";
|
|
resetCursorVersionCache();
|
|
Date.now = () => 1_700_000_000_000;
|
|
|
|
try {
|
|
const headers = executor.buildHeaders({
|
|
accessToken: "prefix::real-token",
|
|
providerSpecificData: { machineId: "machine-1", ghostMode: false },
|
|
});
|
|
|
|
const expectedVersion = getCursorVersion();
|
|
|
|
assert.equal(headers.authorization, "Bearer real-token");
|
|
assert.equal(headers["x-client-key"], generateHashed64Hex("real-token"));
|
|
assert.equal(headers["x-session-id"], generateSessionId("real-token"));
|
|
assert.equal(headers["x-cursor-checksum"], generateCursorChecksum("machine-1"));
|
|
assert.equal(headers["x-cursor-client-version"], expectedVersion);
|
|
assert.equal(headers["x-cursor-user-agent"], `Cursor/${expectedVersion}`);
|
|
assert.equal(headers["user-agent"], `Cursor/${expectedVersion}`);
|
|
assert.equal(headers["x-ghost-mode"], "false");
|
|
assert.equal(headers["connect-protocol-version"], "1");
|
|
assert.match(headers["x-amzn-trace-id"], /^Root=/);
|
|
assert.ok(headers["x-request-id"]);
|
|
} finally {
|
|
Date.now = originalDateNow;
|
|
if (originalDbPath === undefined) {
|
|
delete process.env.CURSOR_STATE_DB_PATH;
|
|
} else {
|
|
process.env.CURSOR_STATE_DB_PATH = originalDbPath;
|
|
}
|
|
resetCursorVersionCache();
|
|
}
|
|
});
|
|
|
|
test("buildCursorHeaders utility stays aligned with Cursor Composer 2 versioned headers", () => {
|
|
const originalDbPath = process.env.CURSOR_STATE_DB_PATH;
|
|
process.env.CURSOR_STATE_DB_PATH = "/nonexistent/cursor/state.vscdb";
|
|
resetCursorVersionCache();
|
|
|
|
try {
|
|
const headers = buildCursorHeaders("prefix::real-token", "machine-1", false);
|
|
const expectedVersion = getCursorVersion();
|
|
|
|
assert.equal(headers.Authorization, "Bearer real-token");
|
|
assert.equal(headers["x-cursor-client-version"], expectedVersion);
|
|
assert.equal(headers["x-cursor-user-agent"], `Cursor/${expectedVersion}`);
|
|
assert.equal(headers["User-Agent"], `Cursor/${expectedVersion}`);
|
|
assert.equal(headers["x-ghost-mode"], "false");
|
|
} finally {
|
|
if (originalDbPath === undefined) {
|
|
delete process.env.CURSOR_STATE_DB_PATH;
|
|
} else {
|
|
process.env.CURSOR_STATE_DB_PATH = originalDbPath;
|
|
}
|
|
resetCursorVersionCache();
|
|
}
|
|
});
|
|
|
|
test("CursorExecutor.buildHeaders derives machineId when not provided", () => {
|
|
const executor = new CursorExecutor();
|
|
const headers = executor.buildHeaders({ accessToken: "real-token", providerSpecificData: {} });
|
|
assert.ok(headers["x-cursor-checksum"], "should have a checksum header");
|
|
assert.ok(headers["x-client-key"], "should have a client key header");
|
|
});
|
|
|
|
test("CursorExecutor.transformRequest produces a framed protobuf payload", () => {
|
|
const executor = new CursorExecutor();
|
|
const transformed = executor.transformRequest(
|
|
"claude-3.5-sonnet",
|
|
{ messages: [{ role: "user", content: "Hello" }], tools: [] },
|
|
true,
|
|
{}
|
|
);
|
|
const frame = parseConnectRPCFrame(transformed);
|
|
const fields = decodeMessage(frame.payload);
|
|
|
|
assert.ok(transformed instanceof Uint8Array);
|
|
assert.equal(frame.flags, 0);
|
|
assert.equal(frame.consumed, transformed.length);
|
|
assert.equal(fields.has(1), true);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToJSON aggregates text and split tool call arguments", async () => {
|
|
const executor = new CursorExecutor();
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
const buffer = Buffer.concat([
|
|
buildTextFrame("Hello "),
|
|
buildToolCallFrame({
|
|
id: "call_1",
|
|
name: "read_file",
|
|
args: '{"path":',
|
|
isLast: false,
|
|
}),
|
|
buildToolCallFrame({
|
|
id: "call_1",
|
|
name: "read_file",
|
|
args: '"/tmp/a"}',
|
|
isLast: true,
|
|
}),
|
|
]);
|
|
|
|
const response = executor.transformProtobufToJSON(buffer, "cursor-small", body);
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(payload.object, "chat.completion");
|
|
assert.equal(payload.model, "cursor-small");
|
|
assert.equal(payload.choices[0].message.content, "Hello ");
|
|
assert.equal(payload.choices[0].finish_reason, "tool_calls");
|
|
assert.equal(payload.choices[0].message.tool_calls[0].function.name, "read_file");
|
|
assert.equal(payload.choices[0].message.tool_calls[0].function.arguments, '{"path":"/tmp/a"}');
|
|
assert.equal(payload.usage.estimated, true);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToJSON finalizes incomplete tool calls when the stream ends early", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToJSON(
|
|
Buffer.concat([
|
|
buildToolCallFrame({
|
|
id: "call_2",
|
|
name: "list_files",
|
|
args: '{"path":"/tmp"}',
|
|
isLast: false,
|
|
}),
|
|
]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(payload.choices[0].finish_reason, "tool_calls");
|
|
assert.equal(payload.choices[0].message.tool_calls[0].id, "call_2");
|
|
assert.equal(payload.choices[0].message.tool_calls[0].function.name, "list_files");
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToJSON keeps prior content when an error frame arrives after output", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToJSON(
|
|
Buffer.concat([
|
|
buildTextFrame("Partial answer"),
|
|
buildJsonErrorFrame({
|
|
error: {
|
|
code: "resource_exhausted",
|
|
message: "late error",
|
|
},
|
|
}),
|
|
]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(payload.choices[0].message.content, "Partial answer");
|
|
assert.equal(payload.choices[0].finish_reason, "stop");
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToJSON decompresses gzip frames", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToJSON(
|
|
Buffer.concat([buildCompressedTextFrame("Compressed answer")]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(payload.choices[0].message.content, "Compressed answer");
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE emits assistant chunks, tool deltas and DONE marker", async () => {
|
|
const executor = new CursorExecutor();
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
const buffer = Buffer.concat([
|
|
buildTextFrame("Hello "),
|
|
buildToolCallFrame({
|
|
id: "call_1",
|
|
name: "read_file",
|
|
args: '{"path":',
|
|
isLast: false,
|
|
}),
|
|
buildToolCallFrame({
|
|
id: "call_1",
|
|
name: "read_file",
|
|
args: '"/tmp/a"}',
|
|
isLast: true,
|
|
}),
|
|
]);
|
|
|
|
const response = executor.transformProtobufToSSE(buffer, "cursor-small", body);
|
|
const text = await response.text();
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.headers.get("Content-Type"), "text/event-stream");
|
|
assert.match(text, /"role":"assistant","content":"Hello "/);
|
|
assert.match(text, /"tool_calls":\[/);
|
|
assert.match(text, /"name":"read_file"/);
|
|
assert.match(text, /"finish_reason":"tool_calls"/);
|
|
assert.match(text, /\[DONE\]/);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE finalizes unterminated tool calls at stream end", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToSSE(
|
|
Buffer.concat([
|
|
buildToolCallFrame({
|
|
id: "call_2",
|
|
name: "read_file",
|
|
args: '{"path":"/tmp/b"}',
|
|
isLast: false,
|
|
}),
|
|
]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const text = await response.text();
|
|
|
|
assert.match(text, /"name":"read_file"/);
|
|
assert.match(text, /"finish_reason":"tool_calls"/);
|
|
assert.match(text, /\[DONE\]/);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE returns a JSON error before any content is streamed", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToSSE(
|
|
buildJsonErrorFrame({
|
|
error: {
|
|
code: "resource_exhausted",
|
|
message: "too many requests",
|
|
details: [{ debug: { error: "LIMIT", details: { title: "Limit hit" } } }],
|
|
},
|
|
}),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 429);
|
|
assert.equal(payload.error.type, "rate_limit_error");
|
|
assert.equal(payload.error.message, "Limit hit");
|
|
assert.equal(payload.error.code, "LIMIT");
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE stops gracefully when a JSON error arrives after content", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToSSE(
|
|
Buffer.concat([
|
|
buildTextFrame("Partial Cursor answer"),
|
|
buildJsonErrorFrame({
|
|
error: {
|
|
code: "resource_exhausted",
|
|
message: "late limit",
|
|
},
|
|
}),
|
|
]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const text = await response.text();
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.match(text, /Partial Cursor answer/);
|
|
assert.match(text, /"finish_reason":"stop"/);
|
|
assert.match(text, /\[DONE\]/);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE emits plain content deltas after tool call chunks", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToSSE(
|
|
Buffer.concat([
|
|
buildToolCallFrame({
|
|
id: "call_3",
|
|
name: "read_file",
|
|
args: '{"path":"/tmp/c"}',
|
|
isLast: false,
|
|
}),
|
|
buildTextFrame("Follow-up text"),
|
|
]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const text = await response.text();
|
|
|
|
assert.match(text, /"name":"read_file"/);
|
|
assert.match(text, /"delta":\{"content":"Follow-up text"\}/);
|
|
assert.match(text, /"finish_reason":"tool_calls"/);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE emits an empty assistant envelope for empty responses", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToSSE(Buffer.alloc(0), "cursor-small", {
|
|
messages: [{ role: "user", content: "hi" }],
|
|
});
|
|
const text = await response.text();
|
|
|
|
assert.match(text, /"role":"assistant","content":""/);
|
|
assert.match(text, /"finish_reason":"stop"/);
|
|
assert.match(text, /\[DONE\]/);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE converts JSON error frames into rate-limit responses", async () => {
|
|
const executor = new CursorExecutor();
|
|
const response = executor.transformProtobufToSSE(
|
|
buildJsonErrorFrame({
|
|
error: {
|
|
code: "resource_exhausted",
|
|
message: "rate limited",
|
|
details: [{ debug: { error: "LIMIT", details: { title: "Limit", detail: "Slow down" } } }],
|
|
},
|
|
}),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 429);
|
|
assert.equal(payload.error.type, "rate_limit_error");
|
|
assert.equal(payload.error.message, "Limit");
|
|
assert.equal(payload.error.code, "LIMIT");
|
|
});
|
|
|
|
test("CursorExecutor.execute returns transformed JSON for non-stream responses", async () => {
|
|
const executor = new CursorExecutor();
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
const responseBuffer = Buffer.concat([buildTextFrame("Hello from Cursor")]);
|
|
executor.makeHttp2Request = async () => ({
|
|
status: 200,
|
|
headers: {},
|
|
body: responseBuffer,
|
|
});
|
|
executor.makeFetchRequest = executor.makeHttp2Request;
|
|
|
|
const result = await executor.execute({
|
|
model: "cursor-small",
|
|
body,
|
|
stream: false,
|
|
credentials: {
|
|
accessToken: "token",
|
|
providerSpecificData: { machineId: "machine-1" },
|
|
},
|
|
});
|
|
const payload = (await result.response.json()) as any;
|
|
|
|
assert.equal(
|
|
result.url,
|
|
"https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
|
);
|
|
assert.equal(result.transformedBody, body);
|
|
assert.equal(result.headers.authorization, "Bearer token");
|
|
assert.equal(payload.object, "chat.completion");
|
|
assert.equal(payload.choices[0].message.content, "Hello from Cursor");
|
|
assert.equal(payload.choices[0].finish_reason, "stop");
|
|
});
|
|
|
|
test("CursorExecutor.execute returns transformed SSE for stream responses", async () => {
|
|
const executor = new CursorExecutor();
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
const responseBuffer = Buffer.concat([buildTextFrame("Hello stream")]);
|
|
executor.makeHttp2Request = async () => ({
|
|
status: 200,
|
|
headers: {},
|
|
body: responseBuffer,
|
|
});
|
|
executor.makeFetchRequest = executor.makeHttp2Request;
|
|
|
|
const result = await executor.execute({
|
|
model: "cursor-small",
|
|
body,
|
|
stream: true,
|
|
credentials: {
|
|
accessToken: "token",
|
|
providerSpecificData: { machineId: "machine-1" },
|
|
},
|
|
});
|
|
const text = await result.response.text();
|
|
|
|
assert.equal(result.response.status, 200);
|
|
assert.match(text, /"content":"Hello stream"/);
|
|
assert.match(text, /\[DONE\]/);
|
|
});
|
|
|
|
test("CursorExecutor.execute maps non-200 upstream responses to OpenAI-style errors", async () => {
|
|
const executor = new CursorExecutor();
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
executor.makeHttp2Request = async () => ({
|
|
status: 403,
|
|
headers: {},
|
|
body: Buffer.from("denied"),
|
|
});
|
|
executor.makeFetchRequest = executor.makeHttp2Request;
|
|
|
|
const result = await executor.execute({
|
|
model: "cursor-small",
|
|
body,
|
|
stream: false,
|
|
credentials: {
|
|
accessToken: "token",
|
|
providerSpecificData: { machineId: "machine-1" },
|
|
},
|
|
});
|
|
const payload = (await result.response.json()) as any;
|
|
|
|
assert.equal(result.response.status, 403);
|
|
assert.equal(payload.error.type, "invalid_request_error");
|
|
assert.match(payload.error.message, /\[403\]: denied/);
|
|
});
|
|
|
|
test("CursorExecutor.execute maps transport failures to connection_error and refreshCredentials returns null", async () => {
|
|
const executor = new CursorExecutor();
|
|
executor.makeHttp2Request = async () => {
|
|
throw new Error("socket hang up");
|
|
};
|
|
executor.makeFetchRequest = executor.makeHttp2Request;
|
|
|
|
const result = await executor.execute({
|
|
model: "cursor-small",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials: {
|
|
accessToken: "token",
|
|
providerSpecificData: { machineId: "machine-1" },
|
|
},
|
|
});
|
|
const payload = (await result.response.json()) as any;
|
|
|
|
assert.equal(result.response.status, 500);
|
|
assert.equal(payload.error.type, "connection_error");
|
|
assert.equal(payload.error.message, "socket hang up");
|
|
assert.equal(await executor.refreshCredentials(), null);
|
|
});
|
|
|
|
test("CursorExecutor.transformProtobufToSSE finalizes un-terminated tools when stream abruptly cuts before isLast", async () => {
|
|
const executor = new CursorExecutor();
|
|
|
|
// Send a tool call but never close it
|
|
const response = executor.transformProtobufToSSE(
|
|
Buffer.concat([
|
|
buildToolCallFrame({
|
|
id: "call_abrupt",
|
|
name: "write_file",
|
|
args: '{"content":"partial"',
|
|
isLast: false,
|
|
}),
|
|
]),
|
|
"cursor-small",
|
|
{ messages: [{ role: "user", content: "hi" }] }
|
|
);
|
|
|
|
const text = await response.text();
|
|
assert.match(text, /"name":"write_file"/);
|
|
assert.match(text, /"finish_reason":"tool_calls"/);
|
|
assert.match(text, /\[DONE\]/);
|
|
});
|