test/cline-compat-emit.test.js
Rama master · réplica de lectura del repositorio original.
import { afterEach, describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import {
toChatCompletion,
streamChatCompletion,
__setStreamChatForTest,
} from '../src/devin-connect-openai.js';
import { getClineCompatStats, resetClineCompatStats } from '../src/handlers/cline-compat.js';
afterEach(() => __setStreamChatForTest(null));
beforeEach(() => resetClineCompatStats());
function fakeStream(events) {
return async function* () { for (const ev of events) yield ev; };
}
// A parameterless tool call: the model emits an EMPTY arguments string. This is
// exactly what Claude does for tools with no parameters, and what
// @ai-sdk/openai-compatible (Cline) silently drops (vercel/ai#6687).
// Native tool calls arrive on the finish event as ev.toolCalls, each
// {id, name, arguments} where arguments is the raw JSON string off the wire.
// Empty string: legacy `|| '{}'` already coalesced this (falsy), so compat adds
// nothing NEW here — the client saw '{}' either way, no repair is counted.
const EMPTY_ARG_TOOLCALL = [
{ type: 'finish', reason: 'tool_calls', usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 },
toolCalls: [{ id: 'call_abc', name: 'list_files', arguments: '' }] },
];
// Whitespace / malformed JSON is TRUTHY, so legacy `|| '{}'` passes it straight
// through and @ai-sdk drops the tool call. This is the case only the compat
// layer rescues — and the one that must be COUNTED as a repair.
const WHITESPACE_ARG_TOOLCALL = [
{ type: 'finish', reason: 'tool_calls', usage: null,
toolCalls: [{ id: 'call_ws', name: 'list_files', arguments: ' ' }] },
];
const MALFORMED_ARG_TOOLCALL = [
{ type: 'finish', reason: 'tool_calls', usage: null,
toolCalls: [{ id: 'call_bad', name: 'do_thing', arguments: '{"a":' }] },
];
describe('Cline compat emit — non-stream (DEVIN_CONNECT path)', () => {
it('WITHOUT compat: empty arguments stay "" (byte-identical legacy behavior)', async () => {
__setStreamChatForTest(fakeStream(EMPTY_ARG_TOOLCALL));
const { body } = await toChatCompletion({ model: 'swe-1-6-slow', messages: [] });
const tc = body.choices[0].message.tool_calls[0];
// Legacy coalesced `argumentsJson || tc.arguments || '{}'`: '' is falsy so
// it already became '{}' historically. Assert the historical value holds.
assert.equal(tc.function.arguments, '{}');
assert.equal(getClineCompatStats().argRepairs, 0, 'no repair counted when compat off');
});
it('WITH compat: empty arguments still "{}" but NOT counted (legacy already coalesced falsy)', async () => {
__setStreamChatForTest(fakeStream(EMPTY_ARG_TOOLCALL));
const { body } = await toChatCompletion({ model: 'swe-1-6-slow', messages: [] }, { clineCompat: true });
assert.equal(body.choices[0].message.tool_calls[0].function.arguments, '{}');
assert.equal(getClineCompatStats().argRepairs, 0, 'empty string was already handled by legacy || {}');
});
it('WITH compat: WHITESPACE arguments rescued to "{}" and counted (legacy would leak " ")', async () => {
__setStreamChatForTest(fakeStream(WHITESPACE_ARG_TOOLCALL));
const { body } = await toChatCompletion({ model: 'swe-1-6-slow', messages: [] }, { clineCompat: true });
assert.equal(body.choices[0].message.tool_calls[0].function.arguments, '{}');
assert.equal(getClineCompatStats().argRepairs, 1, 'whitespace arg is a real repair');
});
it('WITHOUT compat: whitespace arguments leak through as-is (proves the gap exists)', async () => {
__setStreamChatForTest(fakeStream(WHITESPACE_ARG_TOOLCALL));
const { body } = await toChatCompletion({ model: 'swe-1-6-slow', messages: [] });
// Legacy: ' ' is truthy → passes through unrepaired. @ai-sdk drops it.
assert.equal(body.choices[0].message.tool_calls[0].function.arguments, ' ');
assert.equal(getClineCompatStats().argRepairs, 0);
});
it('WITH compat: MALFORMED JSON arguments rescued to "{}" and counted', async () => {
__setStreamChatForTest(fakeStream(MALFORMED_ARG_TOOLCALL));
const { body } = await toChatCompletion({ model: 'swe-1-6-slow', messages: [] }, { clineCompat: true });
assert.equal(body.choices[0].message.tool_calls[0].function.arguments, '{}');
assert.equal(getClineCompatStats().argRepairs, 1);
});
it('WITH compat: already-valid JSON arguments pass through untouched, no repair', async () => {
__setStreamChatForTest(fakeStream([
{ type: 'finish', reason: 'tool_calls', usage: null,
toolCalls: [{ id: 'c1', name: 'get_weather', arguments: '{"city":"Paris"}' }] },
]));
const { body } = await toChatCompletion({ model: 'swe-1-6-slow', messages: [] }, { clineCompat: true });
assert.equal(body.choices[0].message.tool_calls[0].function.arguments, '{"city":"Paris"}');
assert.equal(getClineCompatStats().argRepairs, 0, 'valid JSON is not a repair');
});
});
describe('Cline compat emit — stream (DEVIN_CONNECT path)', () => {
function collectStream(events, opts) {
__setStreamChatForTest(fakeStream(events));
const frames = [];
const send = (obj) => frames.push(obj);
// ToolGuard (allowlist): the connect stream path now filters against
// declared tools[] like the chat main path — declare the ones these
// fixtures emit.
const tools = ['list_files', 'do_thing'].map(name => ({ type: 'function', function: { name } }));
return streamChatCompletion({ model: 'swe-1-6-slow', messages: [], tools }, send, opts).then(() => frames);
}
it('WITH compat: streamed tool_call with whitespace args normalized to "{}" and counted', async () => {
const frames = await collectStream(WHITESPACE_ARG_TOOLCALL, { clineCompat: true });
const toolFrame = frames.find(f => f.choices?.[0]?.delta?.tool_calls);
assert.ok(toolFrame, 'a tool_calls delta frame was emitted');
assert.equal(toolFrame.choices[0].delta.tool_calls[0].function.arguments, '{}');
assert.equal(getClineCompatStats().argRepairs, 1);
});
it('WITHOUT compat: streamed tool_call args stay legacy "{}"', async () => {
const frames = await collectStream(EMPTY_ARG_TOOLCALL, { clineCompat: false });
const toolFrame = frames.find(f => f.choices?.[0]?.delta?.tool_calls);
assert.ok(toolFrame);
assert.equal(toolFrame.choices[0].delta.tool_calls[0].function.arguments, '{}');
assert.equal(getClineCompatStats().argRepairs, 0);
});
});