test/auth-model-scope.test.js
Rama master · réplica de lectura del repositorio original.
// 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);
});
});