Ctrl K
ComunidadGuías…

test/cascade-completion-controls.test.js

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

Archivo original ↗Preguntar a KiBot
// V-14 (KH-005) — the Cascade flow silently dropped every completion control.
//
// MEASURED on the audited baseline: `temperature`, `top_p`, `top_k` and
// `max_tokens` were placed only into the Devin Connect `completionOverrides`;
// neither Cascade entry point received them, and the wire builder hardcoded
// `max_output_tokens = 32768`. A caller asking for a tiny cap therefore got a
// much longer answer that still reported `finish_reason: "stop"` — a lie in the
// one field an auto-continuing client trusts.
//
// The contract this file pins:
//   * the caller's cap reaches the wire (CascadePlannerConfig field 6), clamped
//     to the value the real IDE is observed to send;
//   * the cap is enforced locally as `finish_reason: "length"`, so an upstream
//     that ignores the request cannot turn it into a clean stop;
//   * temperature / top_p / top_k are REFUSED on Cascade with a 400 that names
//     the parameter, because the wire has no field to carry them;
//   * the Devin Connect backend still honours all three (control).

import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  handleChatCompletions,
  __resetConnectDeps,
  __setConnectDeps,
} from '../src/handlers/chat.js';
import {
  buildSendCascadeMessageRequest,
  normalizeCascadeMaxOutputTokens,
  CASCADE_MAX_OUTPUT_TOKENS_CEILING,
} from '../src/windsurf.js';
import { parseFields, getField } from '../src/proto.js';
import { addAccountByKey, getApiKey, removeAccount } from '../src/auth.js';

// The response cache would replay the previous test's capped answer (its key does
// not carry max_tokens) and mask the cap under test.
process.env.RESPONSE_CACHE_ENABLED = '0';

const CALLER = 'api:v14:user:u1';
const CASCADE_MODEL = 'gemini-2.5-flash';
const CONNECT_MODEL = 'swe-1-6-slow';
const created = [];
let savedDevinConnect = null;

function seedAccount(label) {
  const a = addAccountByKey(`v14-${label}-${Math.random().toString(36).slice(2)}`, label);
  created.push(a.id);
  return a;
}

beforeEach(() => { savedDevinConnect = process.env.DEVIN_CONNECT; });

afterEach(() => {
  __resetConnectDeps();
  if (savedDevinConnect === undefined) delete process.env.DEVIN_CONNECT;
  else process.env.DEVIN_CONNECT = savedDevinConnect;
  while (created.length) {
    try { removeAccount(created.pop()); } catch {}
  }
});

/** Read max_output_tokens (field 6) out of the planner config (request field 5). */
function wireMaxOutputTokens(sendProto) {
  const request = parseFields(sendProto);
  const config = getField(request, 5, 2);
  assert.ok(config && config.value && config.value.length, 'the request must carry cascade_config (field 5)');
  // CascadeConfig.planner_config is field 1; max_output_tokens is field 6 of it.
  const plannerWrap = getField(parseFields(config.value), 1, 2);
  assert.ok(plannerWrap && plannerWrap.value, 'cascade_config must carry the planner config (field 1)');
  const field = getField(parseFields(plannerWrap.value), 6, 0);
  return field ? Number(field.value) : null;
}

function cascadeDeps(text, capture) {
  class FakeClient {
    async cascadeChat(_messages, _modelEnum, _modelUid, opts = {}) {
      if (capture) capture.completion = opts.completion;
      if (opts.onChunk) await opts.onChunk({ text, thinking: '', isError: false });
      // The non-stream path reads the returned array; the stream path uses onChunk.
      const chunks = opts.onChunk ? [] : [{ text, thinking: '', isError: false }];
      chunks.toolCalls = [];
      chunks.totalYielded = text.length;
      chunks.totalThinking = 0;
      return chunks;
    }
  }
  return {
    waitForAccount(tried) { return tried.length === 0 ? getApiKey(tried) : null; },
    ensureLs: async () => {},
    getLsFor: () => ({ port: 17991, csrfToken: 'csrf', generation: 1 }),
    WindsurfClient: FakeClient,
    callerKey: CALLER,
  };
}

function fakeRes() {
  const listeners = new Map();
  return {
    body: '',
    writableEnded: false,
    write(chunk) { this.body += String(chunk); return true; },
    end(chunk) {
      if (chunk) this.write(chunk);
      this.writableEnded = true;
      for (const cb of listeners.get('close') || []) cb();
    },
    on(event, cb) { if (!listeners.has(event)) listeners.set(event, []); listeners.get(event).push(cb); return this; },
    once(event, cb) { this.on(event, cb); return this; },
    removeListener() {},
    writeHead() { return this; },
  };
}

function streamFrames(res) {
  return res.body.split('\n\n').filter(Boolean).map((frame) => {
    const line = frame.split('\n').find((l) => l.startsWith('data: '));
    if (!line) return null;
    try { return JSON.parse(line.slice(6)); } catch { return null; }
  }).filter(Boolean);
}

function body(extra = {}) {
  return {
    model: CASCADE_MODEL,
    stream: false,
    messages: [{ role: 'user', content: 'count to a thousand' }],
    ...extra,
  };
}

describe('V-14: the caller output cap reaches the Cascade wire', () => {
  it('writes max_tokens into CascadePlannerConfig field 6', () => {
    const withCap = buildSendCascadeMessageRequest('key', 'cascade-1', 'hi', 0, 'MODEL', 'sess', { maxOutputTokens: 64 });
    assert.equal(wireMaxOutputTokens(withCap), 64);
    const withoutCap = buildSendCascadeMessageRequest('key', 'cascade-1', 'hi', 0, 'MODEL', 'sess', {});
    assert.equal(wireMaxOutputTokens(withoutCap), 32768, 'an unset cap keeps the historical default byte-identically');
  });

  it('clamps to the IDE ceiling and ignores unusable values', () => {
    assert.equal(normalizeCascadeMaxOutputTokens(64), 64);
    assert.equal(normalizeCascadeMaxOutputTokens(1e9), CASCADE_MAX_OUTPUT_TOKENS_CEILING);
    assert.equal(normalizeCascadeMaxOutputTokens(0), CASCADE_MAX_OUTPUT_TOKENS_CEILING);
    assert.equal(normalizeCascadeMaxOutputTokens(-5), CASCADE_MAX_OUTPUT_TOKENS_CEILING);
    assert.equal(normalizeCascadeMaxOutputTokens('nope'), CASCADE_MAX_OUTPUT_TOKENS_CEILING);
    assert.equal(normalizeCascadeOutputFloor(), 1);
  });
});

function normalizeCascadeOutputFloor() {
  // A fractional cap must still ask for at least one token, never zero.
  return normalizeCascadeMaxOutputTokens(0.4);
}

describe('V-14: controls the Cascade wire cannot carry are refused, not dropped', () => {
  it('temperature on Cascade returns 400 param=temperature', async () => {
    process.env.DEVIN_CONNECT = '0';
    seedAccount('cascade-temp');
    const r = await handleChatCompletions(body({ temperature: 0.3 }), cascadeDeps('x'));
    assert.equal(r.status, 400, JSON.stringify(r.body));
    assert.equal(r.body.error.param, 'temperature');
    assert.equal(r.body.error.type, 'invalid_request_error');
    assert.match(r.body.error.message, /temperature/);
  });

  it('top_p and top_k are refused the same way', async () => {
    process.env.DEVIN_CONNECT = '0';
    seedAccount('cascade-topp');
    const topP = await handleChatCompletions(body({ top_p: 0.9 }), cascadeDeps('x'));
    assert.equal(topP.status, 400);
    assert.equal(topP.body.error.param, 'top_p');
    const topK = await handleChatCompletions(body({ top_k: 40 }), cascadeDeps('x'));
    assert.equal(topK.status, 400);
    assert.equal(topK.body.error.param, 'top_k');
  });

  it('control: the Devin Connect backend still honours them', async () => {
    process.env.DEVIN_CONNECT = '1';
    seedAccount('connect-temp');
    let seen = null;
    __setConnectDeps({
      toChatCompletion: async (params) => {
        seen = params.completion || null;
        return {
          status: 200,
          body: {
            id: 'x', object: 'chat.completion', created: 1, model: CONNECT_MODEL,
            choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
          },
        };
      },
    });
    const r = await handleChatCompletions(
      body({ model: CONNECT_MODEL, temperature: 0.3, top_p: 0.9, top_k: 40 }),
      { callerKey: CALLER },
    );
    assert.equal(r.status, 200, JSON.stringify(r.body));
    assert.deepEqual(seen, { temperature: 0.3, topP: 0.9, topK: 40 }, 'the connect adapter must still receive them');
  });
});

describe('V-14: the cap is enforced locally and reported as length', () => {
  const LONG = 'A'.repeat(400); // 400 chars ≈ 100 tokens

  it('non-stream: a small max_tokens truncates the answer and reports length', async () => {
    process.env.DEVIN_CONNECT = '0';
    seedAccount('cascade-cap');
    const capture = {};
    const r = await handleChatCompletions(body({ max_tokens: 10 }), cascadeDeps(LONG, capture));
    assert.equal(r.status, 200, JSON.stringify(r.body));
    assert.deepEqual(capture.completion, { maxTokens: 10 }, 'the cap must reach the producer');
    const choice = r.body.choices[0];
    assert.equal(choice.message.content.length, 40, '10 tokens × 4 chars/token');
    assert.equal(choice.finish_reason, 'length', 'a cut answer is never reported as a clean stop');
  });

  it('non-stream control: no cap means the whole answer and finish_reason stop', async () => {
    process.env.DEVIN_CONNECT = '0';
    seedAccount('cascade-nocap');
    const r = await handleChatCompletions(body({}), cascadeDeps(LONG));
    assert.equal(r.status, 200);
    assert.equal(r.body.choices[0].message.content.length, 400);
    assert.equal(r.body.choices[0].finish_reason, 'stop');
  });

  it('stream: the cap stops the content channel and the terminal says length', async () => {
    process.env.DEVIN_CONNECT = '0';
    seedAccount('cascade-stream-cap');
    const r = await handleChatCompletions(body({ stream: true, max_tokens: 10 }), cascadeDeps(LONG));
    assert.equal(r.status, 200);
    const res = fakeRes();
    await r.handler(res);
    const chunks = streamFrames(res);
    const content = chunks.flatMap((c) => c.choices || []).map((c) => c.delta?.content || '').join('');
    assert.equal(content.length, 40);
    assert.equal(chunks.some((c) => c.choices?.[0]?.finish_reason === 'length'), true,
      `expected a length terminal, got ${JSON.stringify(chunks.map((c) => c.choices?.[0]?.finish_reason ?? c.error?.code))}`);
    assert.equal(chunks.some((c) => c.choices?.[0]?.finish_reason === 'stop'), false);
    assert.ok(res.body.includes('data: [DONE]'), 'a length terminal is still a well-formed stream end');
  });

  it('stream control: no cap streams the whole answer and stops', async () => {
    process.env.DEVIN_CONNECT = '0';
    seedAccount('cascade-stream-nocap');
    const r = await handleChatCompletions(body({ stream: true }), cascadeDeps(LONG));
    const res = fakeRes();
    await r.handler(res);
    const chunks = streamFrames(res);
    const content = chunks.flatMap((c) => c.choices || []).map((c) => c.delta?.content || '').join('');
    assert.equal(content.length, 400);
    assert.equal(chunks.some((c) => c.choices?.[0]?.finish_reason === 'stop'), true);
  });
});