Ctrl K
ComunidadGuías…

test/anthropic-input-validation.test.js

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

Archivo original ↗Preguntar a KiBot
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { validateMessagesRequest, validateCountTokensRequest } from '../src/handlers/messages.js';

// Batch 3 — entry-point input validation (GATE C1/C3/C4/E4). Every rejection
// must be a 400 with error.type === 'invalid_request_error', matching the
// official Anthropic Messages API. Valid inputs return null (no error).

const baseMsgs = [{ role: 'user', content: 'hi' }];
const MODEL = 'claude-sonnet-4.6'; // V1: model is now required, so isolate other checks with a valid model

describe('validateMessagesRequest — V1 model required', () => {
  it('rejects a missing model (was previously accepted, asymmetric with count_tokens)', () => {
    const err = validateMessagesRequest({ max_tokens: 16, messages: baseMsgs });
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /model/);
  });

  it('rejects an empty-string model', () => {
    const err = validateMessagesRequest({ model: '', max_tokens: 16, messages: baseMsgs });
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /model/);
  });

  it('accepts a present model', () => {
    assert.equal(validateMessagesRequest({ model: MODEL, max_tokens: 16, messages: baseMsgs }), null);
  });

  it('rejects a non-string model as invalid_request_error, not a TypeError 500', () => {
    // {"model":123} used to pass the presence check and crash inside
    // resolveModel's `.toLowerCase()` — a 500 for a client shape error.
    for (const model of [123, 0, false, {}, []]) {
      const err = validateMessagesRequest({ model, max_tokens: 16, messages: baseMsgs });
      assert.equal(err.status, 400, `model=${JSON.stringify(model)} must 400`);
      assert.equal(err.body.error.type, 'invalid_request_error');
      assert.equal(err.body.type, 'error');
      assert.match(err.body.error.message, /model/);
    }
  });
});

describe('validateMessagesRequest — C1 max_tokens required + positive integer', () => {
  it('rejects a missing max_tokens (no silent 8192 fallback)', () => {
    const err = validateMessagesRequest({ model: MODEL, messages: baseMsgs });
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /max_tokens/);
  });

  it('rejects max_tokens = 0 (the `||` fallback used to swallow 0)', () => {
    const err = validateMessagesRequest({ model: MODEL, max_tokens: 0, messages: baseMsgs });
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
  });

  it('rejects negative / non-integer / non-number max_tokens', () => {
    for (const mt of [-5, 1.5, '100', NaN, Infinity, null]) {
      const err = validateMessagesRequest({ model: MODEL, max_tokens: mt, messages: baseMsgs });
      assert.equal(err.status, 400, `max_tokens=${mt} should 400`);
      assert.equal(err.body.error.type, 'invalid_request_error');
    }
  });

  it('accepts a positive integer max_tokens', () => {
    assert.equal(validateMessagesRequest({ model: MODEL, max_tokens: 8192, messages: baseMsgs }), null);
    assert.equal(validateMessagesRequest({ model: MODEL, max_tokens: 1, messages: baseMsgs }), null);
  });
});

describe('validateMessagesRequest — C4 cache_control type/ttl validity', () => {
  const withCc = (cc) => ({
    model: MODEL,
    max_tokens: 16,
    messages: [{ role: 'user', content: [{ type: 'text', text: 'x', cache_control: cc }] }],
  });

  it('rejects a non-ephemeral cache_control type', () => {
    const err = validateMessagesRequest(withCc({ type: 'persistent' }));
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /ephemeral/);
  });

  it('rejects an illegal ttl value (used to silently fall back to 5m)', () => {
    const err = validateMessagesRequest(withCc({ type: 'ephemeral', ttl: '30m' }));
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /ttl/);
  });

  it('accepts ephemeral with 5m, 1h, or omitted ttl', () => {
    assert.equal(validateMessagesRequest(withCc({ type: 'ephemeral' })), null);
    assert.equal(validateMessagesRequest(withCc({ type: 'ephemeral', ttl: '5m' })), null);
    assert.equal(validateMessagesRequest(withCc({ type: 'ephemeral', ttl: '1h' })), null);
  });

  it('validates cache_control on tools[] and system[] too', () => {
    const onTool = validateMessagesRequest({
      model: MODEL,
      max_tokens: 16,
      tools: [{ name: 't', cache_control: { type: 'bogus' } }],
      messages: baseMsgs,
    });
    assert.equal(onTool.status, 400);
    const onSystem = validateMessagesRequest({
      model: MODEL,
      max_tokens: 16,
      system: [{ type: 'text', text: 's', cache_control: { type: 'ephemeral', ttl: '2h' } }],
      messages: baseMsgs,
    });
    assert.equal(onSystem.status, 400);
  });
});

describe('validateMessagesRequest — C3 breakpoint count ≤ 4', () => {
  it('accepts exactly 4 breakpoints', () => {
    const body = {
      model: MODEL,
      max_tokens: 16,
      system: [
        { type: 'text', text: 'a', cache_control: { type: 'ephemeral' } },
        { type: 'text', text: 'b', cache_control: { type: 'ephemeral' } },
      ],
      messages: [{ role: 'user', content: [
        { type: 'text', text: 'c', cache_control: { type: 'ephemeral' } },
        { type: 'text', text: 'd', cache_control: { type: 'ephemeral' } },
      ] }],
    };
    assert.equal(validateMessagesRequest(body), null);
  });

  it('rejects a 5th cache_control breakpoint', () => {
    const body = {
      model: MODEL,
      max_tokens: 16,
      system: [
        { type: 'text', text: 'a', cache_control: { type: 'ephemeral' } },
        { type: 'text', text: 'b', cache_control: { type: 'ephemeral' } },
        { type: 'text', text: 'c', cache_control: { type: 'ephemeral' } },
      ],
      messages: [{ role: 'user', content: [
        { type: 'text', text: 'd', cache_control: { type: 'ephemeral' } },
        { type: 'text', text: 'e', cache_control: { type: 'ephemeral' } },
      ] }],
    };
    const err = validateMessagesRequest(body);
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /maximum of 4/);
  });
});

describe('validateCountTokensRequest — E4 model required', () => {
  it('rejects a missing model', () => {
    const err = validateCountTokensRequest({ messages: baseMsgs });
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
    assert.match(err.body.error.message, /model/);
  });

  it('rejects an empty-string model', () => {
    const err = validateCountTokensRequest({ model: '', messages: baseMsgs });
    assert.equal(err.status, 400);
    assert.equal(err.body.error.type, 'invalid_request_error');
  });

  it('accepts a present model', () => {
    assert.equal(validateCountTokensRequest({ model: 'claude-sonnet-4.6', messages: baseMsgs }), null);
  });
});