test/cline-compat.test.js
Rama master · réplica de lectura del repositorio original.
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import {
normalizeToolCallArgs,
detectClineClient,
resolveClineCompat,
recordArgRepair,
getClineCompatStats,
resetClineCompatStats,
stripClineNamespace,
} from '../src/handlers/cline-compat.js';
// cline-compat is the standalone, pluggable Cline compatibility layer. It never
// changes standard /v1 behavior unless a request is explicitly routed through
// /v1/cline/* (endpoint opt-in) or detected as Cline with the master toggle on.
describe('normalizeToolCallArgs', () => {
it('coalesces empty / whitespace / unparseable to "{}"', () => {
// @ai-sdk/openai-compatible drops a tool_call whose arguments fail
// isParsableJson (vercel/ai#6687). Claude sends "" for parameterless tools.
assert.equal(normalizeToolCallArgs(''), '{}');
assert.equal(normalizeToolCallArgs(' '), '{}');
assert.equal(normalizeToolCallArgs('\n\t '), '{}');
assert.equal(normalizeToolCallArgs('{bad'), '{}');
assert.equal(normalizeToolCallArgs('not json'), '{}');
assert.equal(normalizeToolCallArgs(undefined), '{}');
assert.equal(normalizeToolCallArgs(null), '{}');
});
it('preserves already-valid JSON objects and arrays verbatim', () => {
assert.equal(normalizeToolCallArgs('{"city":"Paris"}'), '{"city":"Paris"}');
assert.equal(normalizeToolCallArgs('{}'), '{}');
assert.equal(normalizeToolCallArgs('{"a":1,"b":[2,3]}'), '{"a":1,"b":[2,3]}');
});
it('returns a form that is ALWAYS JSON.parse-able (the @ai-sdk gate), even for JSON-illegal whitespace', () => {
// JS String.trim() strips the full Unicode White_Space set, but JSON only
// permits space/tab/LF/CR. If the function returned the original padded
// string, @ai-sdk's isParsableJson would still reject it and drop the tool
// call. So the RESULT must itself parse. ASCII-space padding is collapsed
// (semantically identical, still valid); JSON-illegal whitespace (NBSP, BOM,
// line/para separators) must not survive in a way that breaks the parse.
for (const input of [' {"a":1} ', ' {}', '{"x":1}', '{}
', ' {"y":2} ']) {
const out = normalizeToolCallArgs(input);
assert.doesNotThrow(() => JSON.parse(out), `result must parse for input ${JSON.stringify(input)} → ${JSON.stringify(out)}`);
}
// ASCII-space padding collapses to the trimmed valid JSON.
assert.equal(normalizeToolCallArgs(' {"a":1} '), '{"a":1}');
});
it('does NOT coerce a bare JSON scalar/array to {} (only object args are valid tool args, but a parseable value passes the SDK gate)', () => {
// The SDK gate is isParsableJson, not is-object. A parseable scalar passes
// it, so we must not rewrite it (that would change the model's intent).
assert.equal(normalizeToolCallArgs('[1,2]'), '[1,2]');
assert.equal(normalizeToolCallArgs('123'), '123');
assert.equal(normalizeToolCallArgs('"x"'), '"x"');
});
});
describe('detectClineClient', () => {
it('detects Cline by User-Agent', () => {
assert.equal(detectClineClient({ 'user-agent': 'Cline/3.2.0' }), true);
assert.equal(detectClineClient({ 'user-agent': 'cline vscode extension' }), true);
assert.equal(detectClineClient({ 'user-agent': 'Mozilla/5.0 ... Cline/1.0' }), true);
});
it('does not misfire on non-Cline clients', () => {
assert.equal(detectClineClient({ 'user-agent': 'curl/8.4.0' }), false);
assert.equal(detectClineClient({ 'user-agent': 'claude-cli/1.0' }), false);
assert.equal(detectClineClient({ 'user-agent': '' }), false);
assert.equal(detectClineClient({}), false);
assert.equal(detectClineClient(null), false);
});
});
describe('resolveClineCompat', () => {
it('endpoint path is active even when master toggle is OFF (explicit opt-in namespace)', () => {
const r = resolveClineCompat({ path: '/v1/cline/chat/completions', headers: {}, masterEnabled: false });
assert.equal(r.active, true);
assert.equal(r.source, 'endpoint');
});
it('standard path with master OFF and non-Cline UA is inactive (byte-identical)', () => {
const r = resolveClineCompat({ path: '/v1/chat/completions', headers: { 'user-agent': 'curl' }, masterEnabled: false });
assert.equal(r.active, false);
});
it('detection path requires master ON', () => {
const off = resolveClineCompat({ path: '/v1/chat/completions', headers: { 'user-agent': 'Cline/3.2' }, masterEnabled: false });
assert.equal(off.active, false);
const on = resolveClineCompat({ path: '/v1/chat/completions', headers: { 'user-agent': 'Cline/3.2' }, masterEnabled: true });
assert.equal(on.active, true);
assert.equal(on.source, 'detect');
});
it('master ON but non-Cline client on standard path stays inactive (no blanket behavior change)', () => {
const r = resolveClineCompat({ path: '/v1/chat/completions', headers: { 'user-agent': 'curl' }, masterEnabled: true });
assert.equal(r.active, false);
});
});
describe('stripClineNamespace', () => {
it('rewrites /v1/cline/* to canonical /v1/*', () => {
assert.equal(stripClineNamespace('/v1/cline/chat/completions'), '/v1/chat/completions');
assert.equal(stripClineNamespace('/v1/cline/models'), '/v1/models');
});
it('leaves non-cline paths unchanged', () => {
assert.equal(stripClineNamespace('/v1/chat/completions'), '/v1/chat/completions');
assert.equal(stripClineNamespace('/v1/models'), '/v1/models');
assert.equal(stripClineNamespace('/dashboard/api/x'), '/dashboard/api/x');
});
});
describe('cline-compat stats', () => {
beforeEach(() => resetClineCompatStats());
it('counts arg repairs', () => {
assert.equal(getClineCompatStats().argRepairs, 0);
recordArgRepair();
recordArgRepair();
assert.equal(getClineCompatStats().argRepairs, 2);
});
});