Ctrl K
ComunidadGuías…

test/cline-compat.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
  });
});