Files
diegosouzapw--omniroute/tests/unit/cursor-agent-tool-calls.test.ts
2026-07-13 13:39:12 +08:00

359 lines
14 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
decodeExecServerEvent,
decodeProtobufValue,
jsonSchemaToProtobufValue,
} from "../../open-sse/utils/cursorAgentProtobuf";
import { newStreamCtx, processFrame, CursorExecutor } from "../../open-sse/executors/cursor";
// ─── Wire-format helpers ───────────────────────────────────────────────────
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wireType: number): Buffer {
return v((field << 3) | wireType);
}
function lenPrefixed(field: number, payload: Buffer): Buffer {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
function stringField(field: number, value: string): Buffer {
return lenPrefixed(field, Buffer.from(value, "utf8"));
}
function varintField(field: number, value: number): Buffer {
return Buffer.concat([tag(field, 0), v(value)]);
}
// Encodes a single map<string, bytes> entry: { key (1): k, value (2): valueBytes }
function mapEntry(field: number, key: string, valueBytes: Buffer): Buffer {
const entry = Buffer.concat([stringField(1, key), lenPrefixed(2, valueBytes)]);
return lenPrefixed(field, entry);
}
// AgentServerMessage { exec_server_message (2): ESM { id, exec_id, mcp_args (11): ... } }
function buildMcpArgsEvent(
execMsgId: number,
execId: string,
toolName: string,
toolCallId: string,
args: Record<string, unknown>
): Buffer {
const mcaParts: Buffer[] = [
stringField(1, toolName), // name
stringField(3, toolCallId), // tool_call_id
];
for (const [k, val] of Object.entries(args)) {
const valueBytes = jsonSchemaToProtobufValue(val);
mcaParts.push(mapEntry(2, k, valueBytes)); // args = field 2
}
const mca = Buffer.concat(mcaParts);
const esm = Buffer.concat([
varintField(1, execMsgId),
stringField(15, execId),
lenPrefixed(11, mca),
]);
return lenPrefixed(2, esm);
}
// ─── decodeProtobufValue round-trip tests ──────────────────────────────────
test("decodeProtobufValue round-trips primitives", () => {
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue("hi") as Buffer), "hi");
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(42.5)), 42.5);
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(true)), true);
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(false)), false);
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(null)), null);
});
test("decodeProtobufValue round-trips a flat object", () => {
const obj = { name: "alice", age: 30, active: true };
const encoded = jsonSchemaToProtobufValue(obj);
assert.deepEqual(decodeProtobufValue(encoded), obj);
});
test("decodeProtobufValue round-trips an array of strings", () => {
const arr = ["a", "b", "c"];
const encoded = jsonSchemaToProtobufValue(arr);
assert.deepEqual(decodeProtobufValue(encoded), arr);
});
test("decodeProtobufValue round-trips a nested object with mixed types", () => {
const obj = {
city: "Paris",
coords: { lat: 48.8566, lng: 2.3522 },
tags: ["europe", "capital"],
populated: true,
sister_city: null,
};
const encoded = jsonSchemaToProtobufValue(obj);
assert.deepEqual(decodeProtobufValue(encoded), obj);
});
test("decodeProtobufValue handles deeply nested OpenAI tool args", () => {
const args = {
location: { city: "Tokyo", country: "JP" },
units: "metric",
days: 7,
};
const encoded = jsonSchemaToProtobufValue(args);
assert.deepEqual(decodeProtobufValue(encoded), args);
});
// ─── McpArgs decoding tests ────────────────────────────────────────────────
test("decodeExecServerEvent populates args dict from McpArgs map entries", () => {
const args = { city: "Paris", units: "celsius" };
const event = decodeExecServerEvent(
buildMcpArgsEvent(1, "exec-w", "get_weather", "call_abc", args)
);
assert.equal(event?.kind, "exec_mcp");
if (event?.kind !== "exec_mcp") return;
assert.equal(event.toolName, "get_weather");
assert.equal(event.toolCallId, "call_abc");
assert.deepEqual(event.args, args);
});
test("decodeExecServerEvent handles McpArgs with empty args", () => {
const event = decodeExecServerEvent(buildMcpArgsEvent(1, "exec-x", "no_args_tool", "call_x", {}));
if (event?.kind !== "exec_mcp") throw new Error("expected exec_mcp");
assert.deepEqual(event.args, {});
});
test("decodeExecServerEvent handles McpArgs with nested object args", () => {
const args = {
location: { city: "London", country: "UK" },
days: 3,
};
const event = decodeExecServerEvent(buildMcpArgsEvent(1, "exec-n", "weather", "call_n", args));
if (event?.kind !== "exec_mcp") throw new Error("expected exec_mcp");
assert.deepEqual(event.args, args);
});
// ─── Streaming SSE tool_calls emission ─────────────────────────────────────
function parseChunk(line: string): {
delta?: {
tool_calls?: Array<{
index: number;
id?: string;
function?: { name?: string; arguments?: string };
}>;
};
finish_reason?: string | null;
} {
return JSON.parse(line.replace(/^data: /, "").trim()).choices[0];
}
test("processFrame emits OpenAI tool_calls deltas for an mcp_args event", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const payload = buildMcpArgsEvent(1, "exec-tc", "get_weather", "cursor_call_x", {
city: "Paris",
});
processFrame(payload, ctx, new Set());
// Expect: role chunk + tool_calls init chunk + tool_calls args chunk
assert.equal(emitted.length, 3);
const roleChunk = parseChunk(emitted[0]);
assert.ok(roleChunk.delta);
// Init chunk: id + name
const initChunk = parseChunk(emitted[1]);
const initCall = initChunk.delta?.tool_calls?.[0];
assert.equal(initCall?.index, 0);
assert.match(initCall?.id ?? "", /^call_/);
assert.equal(initCall?.function?.name, "get_weather");
assert.equal(initCall?.function?.arguments, "");
// Args chunk
const argsChunk = parseChunk(emitted[2]);
const argsCall = argsChunk.delta?.tool_calls?.[0];
assert.equal(argsCall?.index, 0);
assert.equal(argsCall?.function?.arguments, JSON.stringify({ city: "Paris" }));
// Side-effects on ctx
assert.equal(ctx.toolCalls.length, 1);
assert.equal(ctx.toolCalls[0].name, "get_weather");
assert.equal(ctx.emittedToolCallIndex, 1);
});
test("processFrame indexes parallel tool calls sequentially", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const acked = new Set<string>();
processFrame(buildMcpArgsEvent(1, "exec-1", "tool_a", "ca", { x: 1 }), ctx, acked);
processFrame(buildMcpArgsEvent(2, "exec-2", "tool_b", "cb", { y: 2 }), ctx, acked);
assert.equal(ctx.toolCalls.length, 2);
assert.equal(ctx.emittedToolCallIndex, 2);
// emitted[0] = role chunk
// emitted[1] = init for tool_a (index=0)
// emitted[2] = args for tool_a (index=0)
// emitted[3] = init for tool_b (index=1)
// emitted[4] = args for tool_b (index=1)
const initB = parseChunk(emitted[3]);
assert.equal(initB.delta?.tool_calls?.[0].index, 1);
assert.equal(initB.delta?.tool_calls?.[0].function?.name, "tool_b");
});
test("processFrame emits text + tool_calls in mixed order", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const acked = new Set<string>();
// Build a text_delta payload
function textDelta(text: string): Buffer {
const tdu = lenPrefixed(1, Buffer.from(text, "utf8"));
const iu = lenPrefixed(1, tdu);
return lenPrefixed(1, iu);
}
processFrame(textDelta("Let me check"), ctx, acked);
processFrame(buildMcpArgsEvent(1, "exec-mix", "lookup", "c1", {}), ctx, acked);
assert.equal(ctx.totalText, "Let me check");
assert.equal(ctx.toolCalls.length, 1);
// Order: role chunk, content chunk, tool_call init, tool_call args
assert.equal(emitted.length, 4);
const contentChunk = JSON.parse(emitted[1].replace(/^data: /, "").trim());
assert.equal(contentChunk.choices[0].delta.content, "Let me check");
const initChunk = JSON.parse(emitted[2].replace(/^data: /, "").trim());
assert.ok(initChunk.choices[0].delta.tool_calls);
});
test("processFrame doesn't emit tool_calls for the same exec_id twice", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const acked = new Set<string>();
const payload = buildMcpArgsEvent(1, "exec-dup", "tool", "cd", {});
processFrame(payload, ctx, acked);
processFrame(payload, ctx, acked);
assert.equal(ctx.toolCalls.length, 1);
});
// ─── Tool-commit directive (ported from composer-api) ──────────────────────
//
// transformRequest() returns the encoded Connect-RPC body; the UserMessage.text
// is plain UTF-8 inside it, so we can assert the directive is present/absent.
const weatherTool = {
type: "function",
function: {
name: "web_search",
description: "Search the web",
parameters: { type: "object", properties: { q: { type: "string" } } },
},
};
test("transformRequest prepends the tool-commit directive when tools are declared", () => {
const exec = new CursorExecutor();
const body = exec.transformRequest(
"composer-2.5",
{ messages: [{ role: "user", content: "how's the weather?" }], tools: [weatherTool] },
false,
{}
) as Uint8Array;
const text = Buffer.from(body).toString("utf8");
assert.ok(text.includes("you MUST issue the actual tool call"), "directive present with tools");
assert.ok(text.includes("how's the weather?"), "original user text preserved");
});
test("transformRequest omits the directive when no tools are declared", () => {
const exec = new CursorExecutor();
const body = exec.transformRequest(
"composer-2.5",
{ messages: [{ role: "user", content: "hi there" }] },
false,
{}
) as Uint8Array;
const text = Buffer.from(body).toString("utf8");
assert.ok(!text.includes("you MUST issue the actual tool call"), "no directive without tools");
assert.ok(text.includes("hi there"), "user text present");
});
test("transformRequest honors CURSOR_TOOL_DIRECTIVE=0 opt-out", () => {
const prev = process.env.CURSOR_TOOL_DIRECTIVE;
process.env.CURSOR_TOOL_DIRECTIVE = "0";
try {
const exec = new CursorExecutor();
const body = exec.transformRequest(
"composer-2.5",
{ messages: [{ role: "user", content: "weather?" }], tools: [weatherTool] },
false,
{}
) as Uint8Array;
const text = Buffer.from(body).toString("utf8");
assert.ok(!text.includes("you MUST issue the actual tool call"), "directive suppressed by opt-out");
} finally {
if (prev === undefined) delete process.env.CURSOR_TOOL_DIRECTIVE;
else process.env.CURSOR_TOOL_DIRECTIVE = prev;
}
});
// ─── tool_choice handling (ported from composer-api) ───────────────────────
function encodedText(body: Record<string, unknown>): string {
const exec = new CursorExecutor();
const buf = exec.transformRequest("composer-2.5", body, false, {}) as Uint8Array;
return Buffer.from(buf).toString("utf8");
}
test("tool_choice:'none' drops tools and the directive", () => {
const text = encodedText({
messages: [{ role: "user", content: "weather?" }],
tools: [weatherTool],
tool_choice: "none",
});
assert.ok(!text.includes("you MUST issue the actual tool call"), "no directive when tool_choice none");
assert.ok(!text.includes("web_search"), "tool not advertised when tool_choice none");
});
test("tool_choice:'required' adds the forcing line", () => {
const text = encodedText({
messages: [{ role: "user", content: "weather?" }],
tools: [weatherTool],
tool_choice: "required",
});
assert.ok(text.includes("you MUST issue the actual tool call"), "base directive present");
assert.ok(text.includes("at least one of the available tools"), "required forcing line present");
});
test("tool_choice specific-function forces that tool by name", () => {
const text = encodedText({
messages: [{ role: "user", content: "weather?" }],
tools: [weatherTool],
tool_choice: { type: "function", function: { name: "web_search" } },
});
assert.ok(text.includes("You MUST call the `web_search` tool now"), "specific tool forced");
});
// ─── output constraints (ported from composer-api) ─────────────────────────
test("response_format json_object adds a JSON output constraint", () => {
const text = encodedText({
messages: [{ role: "user", content: "give me a profile" }],
response_format: { type: "json_object" },
});
assert.ok(text.includes("OUTPUT CONSTRAINTS:"), "constraints block present");
assert.ok(text.includes("single valid JSON object"), "json_object constraint present");
});
test("max_tokens and stop are surfaced as output constraints", () => {
const text = encodedText({
messages: [{ role: "user", content: "hi" }],
max_tokens: 128,
stop: ["END"],
});
assert.ok(text.includes("within about 128 output tokens"), "max_tokens constraint present");
assert.ok(text.includes("Stop before any of these sequences: END"), "stop constraint present");
});
test("no output constraints block when no constraining params are set", () => {
const text = encodedText({ messages: [{ role: "user", content: "hi" }] });
assert.ok(!text.includes("OUTPUT CONSTRAINTS:"), "no constraints block by default");
});