d34618003d
Allow chat core callers to disable the emergency fallback path during routed retries and expose proxy cache reset helpers for deterministic state handling. Add regression coverage for chat routing edge cases, combo strategies, stream utilities, cursor SSE termination, and JSON-to-SQLite db migration behavior.
507 lines
16 KiB
JavaScript
507 lines
16 KiB
JavaScript
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 {
|
|
generateCursorChecksum,
|
|
generateHashed64Hex,
|
|
generateSessionId,
|
|
} from "../../open-sse/utils/cursorChecksum.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;
|
|
Date.now = () => 1_700_000_000_000;
|
|
|
|
try {
|
|
const headers = executor.buildHeaders({
|
|
accessToken: "prefix::real-token",
|
|
providerSpecificData: { machineId: "machine-1", ghostMode: false },
|
|
});
|
|
|
|
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-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;
|
|
}
|
|
});
|
|
|
|
test("CursorExecutor.buildHeaders requires a machine ID", () => {
|
|
const executor = new CursorExecutor();
|
|
assert.throws(
|
|
() => executor.buildHeaders({ accessToken: "real-token", providerSpecificData: {} }),
|
|
/Machine ID is required/
|
|
);
|
|
});
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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\]/);
|
|
});
|