Ctrl K
ComunidadGuías…

test/auth-model-scope.test.js

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

Archivo original ↗Preguntar a KiBot
// Q2 T10 — a model-scoped 429 with an upstream reset window must bench the
// account for THAT model only, never for the rest of the pool.
//
// The auth layer already writes `_modelRateLimits[modelKey]` when markRateLimited
// is given a model key; the defect this file pins is the POOL-LEVEL view:
// `isAllRateLimited(null, selector)` ignored the selector dimension entirely, so a
// connect preflight reported "available" for a pool whose every account was
// cooled for that selector — spending the full queue wait and then failing with a
// generic pool error instead of the real Retry-After. (The call site that decides
// account-wide vs model-scoped for an upstream 429 lives in chat.js
// finalizeConnectAccount; this file covers the auth-side invariant it relies on.)
import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  addAccountByKey, removeAccount, markRateLimited, releaseAccount,
  getAccountInternal, getApiKey, isAllRateLimited, isAllTemporarilyUnavailable,
  __isRateLimitedForModel,
} from '../src/auth.js';
import { setBreakerTunables } from '../src/runtime-config.js';

// Both are in the default (unknown-tier) catalog, so the eligibility filter in
// isAllRateLimited does not skip the account for an unrelated reason.
const MODEL_A = 'claude-4-sonnet';
const MODEL_B = 'claude-4.5-haiku';
const SELECTOR_A = 'swe-1-6-slow';
const SELECTOR_B = 'gpt-5-5-sol';
const RESET_MS = 3 * 60 * 60 * 1000;

const created = [];
function add(label) {
  const account = addAccountByKey(
    `t10-key-${Date.now()}-${created.length}-${Math.random().toString(36).slice(2)}`,
    label,
  );
  created.push(account.id);
  return account;
}

afterEach(() => {
  // The fully-throttled fallback is ON by default and would hand back a cooled
  // account as an explicit `_degraded` serve; these assertions are about hard
  // exclusion, so pin the opt-out.
  setBreakerTunables({ degradedServe: null });
  while (created.length) removeAccount(created.pop());
});

describe('T10 — a model-scoped 429 does not evict the account for other models', () => {
  it('writes only the model dimension, never the account-wide one', () => {
    const account = add('model-scope');
    markRateLimited(account.apiKey, RESET_MS, MODEL_A, 'r');

    const live = getAccountInternal(account.id);
    assert.equal(live.rateLimitedUntil || 0, 0, 'the account-wide latch must stay clear');
    assert.equal(live.quotaResetAt || 0, 0, 'a rate limit is not a quota dry-well');
    assert.ok(live._modelRateLimits?.[MODEL_A] > Date.now(), 'the reset window is stored per model');
    assert.equal(live._modelRateLimits?.[MODEL_B], undefined, 'no cooldown is invented for other models');
  });

  it('isAllRateLimited reports the cooled model only', () => {
    const account = add('all-limited');
    markRateLimited(account.apiKey, RESET_MS, MODEL_A, 'r');

    const cooled = isAllRateLimited(MODEL_A);
    assert.equal(cooled.allLimited, true, 'the only account is cooled for MODEL_A');
    assert.ok(cooled.retryAfterMs > RESET_MS - 60_000 && cooled.retryAfterMs <= RESET_MS,
      `Retry-After must carry the upstream reset, got ${cooled.retryAfterMs}`);

    assert.equal(isAllRateLimited(MODEL_B).allLimited, false,
      'a cooldown for MODEL_A must not black out MODEL_B');
  });

  it('the selection gate still hands the account to other models', () => {
    setBreakerTunables({ degradedServe: false });
    const account = add('select');
    markRateLimited(account.apiKey, RESET_MS, MODEL_A, 'r');

    const other = getApiKey([], MODEL_B, 't10-other');
    assert.ok(other, 'MODEL_B must remain routable');
    assert.equal(other.apiKey, account.apiKey);
    releaseAccount(other.apiKey);

    assert.equal(getApiKey([], MODEL_A, 't10-cooled'), null,
      'MODEL_A must not be handed to a new request on the cooled account');
  });

  it('the connect preflight sees a selector-scoped cooldown (isAllRateLimited null + selector)', () => {
    const account = add('selector-scope');
    // The connect path selects with modelKey=null and stores the cooldown under
    // the resolved selector. Before the fix this lookup was blind to it.
    markRateLimited(account.apiKey, RESET_MS, SELECTOR_A, 'c');

    const cooled = isAllRateLimited(null, SELECTOR_A);
    assert.equal(cooled.allLimited, true, 'a selector-scoped cooldown must gate the connect preflight');
    assert.ok(cooled.retryAfterMs > RESET_MS - 60_000 && cooled.retryAfterMs <= RESET_MS,
      'and its Retry-After must be the upstream window, not the 60s default');

    assert.equal(isAllRateLimited(null, SELECTOR_B).allLimited, false,
      'other selectors on the same account stay available');
    assert.equal(isAllRateLimited(null).allLimited, false,
      'the account is not benched pool-wide');
  });

  it('isAllTemporarilyUnavailable sees the same selector dimension', () => {
    // The chat fast-429 preflight ORs rl and tu and treats `rl && !tu` as
    // degradable. A selector-cooled pool must report BOTH true, or the preflight
    // skips the fast 429 and spends the queue wait before failing.
    const account = add('tu-selector');
    markRateLimited(account.apiKey, RESET_MS, SELECTOR_A, 'c');

    const cooled = isAllTemporarilyUnavailable(null, SELECTOR_A);
    assert.equal(cooled.allUnavailable, true, 'a selector-scoped cooldown is temporary unavailability');
    assert.ok(cooled.retryAfterMs > RESET_MS - 60_000 && cooled.retryAfterMs <= RESET_MS,
      `Retry-After must be the upstream window, got ${cooled.retryAfterMs}`);

    assert.equal(isAllTemporarilyUnavailable(null, SELECTOR_B).allUnavailable, false,
      'other selectors remain available');
    assert.equal(isAllTemporarilyUnavailable(null).allUnavailable, false,
      'and the account is not globally unavailable');
  });

  it('the connect lookup honours the selector dimension (control: other selector served)', () => {
    setBreakerTunables({ degradedServe: false });
    const account = add('selector-select');
    markRateLimited(account.apiKey, RESET_MS, SELECTOR_A, 'c');

    const other = getApiKey([], null, 't10-selector-other', SELECTOR_B);
    assert.ok(other, 'SELECTOR_B must remain routable');
    assert.equal(other.apiKey, account.apiKey);
    releaseAccount(other.apiKey);

    assert.equal(getApiKey([], null, 't10-selector-cooled', SELECTOR_A), null,
      'SELECTOR_A must not be handed out while its cooldown is live');
    assert.equal(__isRateLimitedForModel(getAccountInternal(account.id), SELECTOR_A), true);
    assert.equal(__isRateLimitedForModel(getAccountInternal(account.id), SELECTOR_B), false);
  });
});