Ctrl K
ComunidadGuías…

test/devin-connect-strict-model.test.js

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

Archivo original ↗Preguntar a KiBot
// P1 whitelist guard: on the DEVIN_CONNECT path, a model name that resolves to
// NO real upstream selector (resolveConnectSelector → mapped:false) must be
// rejected with 400 model_not_found BEFORE any upstream call, instead of silently
// degrading to the free-tier selector (swe-1-6-slow). Silent degrade means the
// client's opus/gpt request quietly runs a different free model — wrong output,
// wrong billing — and a junk name can trip UPSTREAM_INTERNAL and burn account
// health. Gated by WINDSURFAPI_STRICT_MODEL (default on).

import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { addAccountByKey, removeAccount } from '../src/auth.js';
import { handleChatCompletions } from '../src/handlers/chat.js';

const createdAccountIds = [];

afterEach(() => {
  delete process.env.DEVIN_CONNECT;
  delete process.env.WINDSURFAPI_STRICT_MODEL;
  while (createdAccountIds.length) removeAccount(createdAccountIds.pop());
});

function withAccount(label) {
  const a = addAccountByKey(`strict-${Date.now()}-${Math.random().toString(36).slice(2)}`, label);
  createdAccountIds.push(a.id);
  return a;
}

describe('DEVIN_CONNECT strict model whitelist (P1)', () => {
  it('rejects an unmapped model with 400 model_not_found (no upstream call)', async () => {
    process.env.DEVIN_CONNECT = '1';
    withAccount('strict-reject');
    const body = {
      model: 'totally-made-up-model-xyz',
      messages: [{ role: 'user', content: 'hi' }],
    };
    const result = await handleChatCompletions(body);
    assert.equal(result.status, 400);
    assert.equal(result.body.error.code, 'model_not_found');
    assert.equal(result.body.error.param, 'model');
    assert.match(result.body.error.message, /not a valid model/i);
  });

  it('does NOT reject a real mapped selector', async () => {
    process.env.DEVIN_CONNECT = '1';
    withAccount('strict-allow');
    // claude-5-fable-medium is a real catalog selector → mapped:true → must pass
    // the guard (it will proceed to the upstream path, which we don't stub here;
    // the assertion is only that it is NOT the 400 model_not_found reject).
    const body = {
      model: 'claude-5-fable-medium',
      stream: false,
      messages: [{ role: 'user', content: 'hi' }],
    };
    const result = await handleChatCompletions(body);
    assert.notEqual(
      result.status === 400 && result.body?.error?.code === 'model_not_found',
      true,
      'a valid selector must not be rejected by the whitelist guard',
    );
  });

  it('WINDSURFAPI_STRICT_MODEL=0 restores the legacy silent-degrade (no 400)', async () => {
    process.env.DEVIN_CONNECT = '1';
    process.env.WINDSURFAPI_STRICT_MODEL = '0';
    withAccount('strict-optout');
    const body = {
      model: 'totally-made-up-model-xyz',
      messages: [{ role: 'user', content: 'hi' }],
    };
    const result = await handleChatCompletions(body);
    // With the guard off, it must NOT 400-model_not_found (it degrades instead).
    assert.notEqual(
      result.status === 400 && result.body?.error?.code === 'model_not_found',
      true,
      'opt-out must not reject; it degrades to free tier as before',
    );
  });
});

// Regression: the server-side default model (used when a request omits `model`)
// MUST resolve to a live DEVIN_CONNECT selector, or every omitted-model request
// on a DEVIN_CONNECT deployment hits the strict guard above → 400 / free downgrade.
// The old default 'claude-4.5-sonnet-thinking' was mapped:false and did exactly
// that. This locks the default to a mapped name so a future edit can't regress it.
describe('config.defaultModel must be mapped on the DEVIN_CONNECT path', () => {
  it('resolveConnectSelector(config.defaultModel) is mapped:true', async () => {
    const { config } = await import('../src/config.js');
    const { resolveConnectSelector } = await import('../src/devin-connect-models.js');
    const r = resolveConnectSelector(config.defaultModel);
    assert.equal(r.mapped, true, `default model "${config.defaultModel}" must map to a live selector (got ${JSON.stringify(r)})`);
  });

  it('an omitted-model request is NOT rejected by the strict whitelist guard', async () => {
    process.env.DEVIN_CONNECT = '1';
    withAccount('strict-default');
    const body = { messages: [{ role: 'user', content: 'hi' }] }; // no model → server default
    const result = await handleChatCompletions(body);
    assert.notEqual(
      result.status === 400 && result.body?.error?.code === 'model_not_found',
      true,
      'omitting model must fall through to a usable server default, not a 400',
    );
  });
});