Ctrl K
ComunidadGuías…

test/devin-connect-pool.test.js

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

Archivo original ↗Preguntar a KiBot
import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { addAccountByKey, getApiKey, getRpmStats, removeAccount, getAccountList, __setReloginDeps, __resetReloginState } from '../src/auth.js';
import { finalizeConnectAccount } from '../src/handlers/chat.js';
import { setBreakerTunables } from '../src/runtime-config.js';
import { getStats } from '../src/dashboard/stats.js';

// DEVIN_CONNECT account lifecycle: acquireConnectAccount draws from the same
// pool as Cascade (verified end-to-end against a live token elsewhere); these
// tests cover finalizeConnectAccount's bookkeeping without touching the network.

const createdIds = [];
afterEach(() => { while (createdIds.length) removeAccount(createdIds.pop()); });

function seed(label) {
  const key = `devin-session-token$pool-${label}-${Math.random().toString(36).slice(2)}`;
  const acct = addAccountByKey(key, label);
  createdIds.push(acct.id);
  return acct;
}

describe('finalizeConnectAccount', () => {
  it('releases a pooled account back (inflight returns to 0) on success', () => {
    seed('release-ok');
    const acct = getApiKey([], null, ''); // acquire: bumps inflight + rpm
    assert.ok(acct, 'acquired an account');
    const usedAfterAcquire = getRpmStats()[acct.id]?.used ?? 0;
    assert.ok(usedAfterAcquire >= 1, 'rpm budget consumed on acquire');

    finalizeConnectAccount(acct, { model: 'swe-1-6-slow', startTime: Date.now() - 5, err: null });
    // releaseAccount decrements inflight; the account is selectable again.
    const reacquire = getApiKey([], null, '');
    assert.ok(reacquire, 'account is selectable again after release');
  });

  it('records a successful request in dashboard stats', () => {
    const acct = seed('stats-ok');
    const before = getStats().totalRequests;
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now() - 5, err: null },
    );
    assert.equal(getStats().totalRequests, before + 1);
  });

  it('records a failed request and does not throw on error finalize', () => {
    const acct = seed('stats-err');
    const before = getStats().errorCount;
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now() - 5, err: Object.assign(new Error('x'), { code: 'UNAUTHORIZED' }) },
    );
    assert.equal(getStats().errorCount, before + 1);
  });

  it('handles a null account (env-token fallback) by recording stats only', () => {
    const before = getStats().totalRequests;
    // must not throw when there is no pooled account to release.
    finalizeConnectAccount(null, { model: 'swe-1-6-slow', startTime: Date.now() - 5, err: null });
    assert.equal(getStats().totalRequests, before + 1);
  });

  it('treats a RATE_LIMITED error without throwing', () => {
    const acct = seed('rate');
    assert.doesNotThrow(() => finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now(), err: Object.assign(new Error('rl'), { code: 'RATE_LIMITED' }) },
    ));
  });

  // F2: a BARE 429 (no upstream reset window) applies an account-wide cooldown of
  // `rlBurstMs`. Default is 15000 (15s) as of 2026-07-12 (429 mitigation on) — the
  // historical value was 300000 (5min). An operator can revert via the tunable. A
  // 429 WITH a reset window also cools account-wide, for the real window duration.
  it('F2: bare 429 default cools the account ~15s account-wide', () => {
    const acct = seed('rl-bare-default');
    const t0 = Date.now();
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: t0, err: Object.assign(new Error('rate limited'), { code: 'RATE_LIMITED' }) },
    );
    const until = acct.rateLimitedUntil || 0;
    assert.ok(until > t0 + 10 * 1000 && until < t0 + 20 * 1000, `account-wide ~15s (got ${(until - t0) / 1000}s)`);
  });

  it('F2: rlBurstMs override shortens the bare-429 account-wide cooldown', () => {
    setBreakerTunables({ rlBurstMs: 15000 });
    try {
      const acct = seed('rl-bare-short');
      const t0 = Date.now();
      finalizeConnectAccount(
        { id: acct.id, apiKey: acct.apiKey },
        { model: 'swe-1-6-slow', startTime: t0, err: Object.assign(new Error('rate limited'), { code: 'RATE_LIMITED' }) },
      );
      const until = acct.rateLimitedUntil || 0;
      assert.ok(until > t0 + 10 * 1000 && until < t0 + 20 * 1000, `shortened to ~15s (got ${(until - t0) / 1000}s)`);
    } finally {
      setBreakerTunables({ rlBurstMs: null });
    }
  });

  it('F2: a 429 WITH a reset window cools the throttled selector for the real window, ignoring rlBurstMs', () => {
    setBreakerTunables({ rlBurstMs: 15000 });
    try {
      const acct = seed('rl-window');
      const t0 = Date.now();
      finalizeConnectAccount(
        { id: acct.id, apiKey: acct.apiKey },
        { model: 'swe-1-6-slow', startTime: t0, err: Object.assign(new Error('resets in 3h'), { code: 'RATE_LIMITED', resetMs: 3 * 60 * 60 * 1000 }) },
      );
      // T10: the reset window is scoped to the throttled model/selector, not the
      // whole account. Connect selection consults the selector dimension through
      // isCooledForRequest (it no longer relies on rateLimitedUntil alone), so a
      // model-scoped window IS visible to the pool while every other model stays
      // eligible on the same account.
      assert.ok(acct._modelRateLimits?.['swe-1-6-slow'] > t0 + 2 * 60 * 60 * 1000, 'selector cooldown honours the 3h window');
      assert.ok(!(acct.rateLimitedUntil > t0), 'the account-wide dimension must stay clear');
    } finally {
      setBreakerTunables({ rlBurstMs: null });
    }
  });

  it('fires a background re-login on UNAUTHORIZED when auto-relogin is configured', async () => {
    const acct = seed('relogin-trigger');
    acct.email = `relogin-${acct.id}@example.com`;
    acct.method = 'email';
    process.env.DEVIN_CONNECT_AUTO_RELOGIN = '1';
    __resetReloginState();
    let loginCalls = 0;
    __setReloginDeps({
      isCredStoreEnabled: () => true,
      getCredential: () => 'pw',
      windsurfLogin: async () => { loginCalls++; return { apiKey: 'devin-session-token$RECOVERED' }; },
    });
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now(), err: Object.assign(new Error('401'), { code: 'UNAUTHORIZED' }) },
    );
    // finalize fires re-login fire-and-forget; let the microtask/import settle.
    await new Promise(r => setTimeout(r, 30));
    assert.equal(loginCalls, 1, 'UNAUTHORIZED triggered exactly one re-login');
    assert.equal(acct.apiKey, 'devin-session-token$RECOVERED', 'fresh token swapped in');
    __setReloginDeps(null);
    __resetReloginState();
    delete process.env.DEVIN_CONNECT_AUTO_RELOGIN;
  });

  it('does not re-login on UNAUTHORIZED when auto-relogin is off', async () => {
    const acct = seed('relogin-off');
    acct.email = `off-${acct.id}@example.com`;
    delete process.env.DEVIN_CONNECT_AUTO_RELOGIN;
    __resetReloginState();
    let loginCalls = 0;
    __setReloginDeps({
      isCredStoreEnabled: () => true,
      getCredential: () => 'pw',
      windsurfLogin: async () => { loginCalls++; return { apiKey: 'x' }; },
    });
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now(), err: Object.assign(new Error('401'), { code: 'UNAUTHORIZED' }) },
    );
    await new Promise(r => setTimeout(r, 30));
    assert.equal(loginCalls, 0);
    __setReloginDeps(null);
  });

  it('does not penalize the account on a client abort', () => {
    const acct = seed('abort');
    const before = getStats().errorCount;
    // AbortError is a client cancel, not an account fault — released, recorded
    // as a failed request for stats, but no reportError penalty.
    assert.doesNotThrow(() => finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now(), err: Object.assign(new Error('aborted'), { name: 'AbortError' }) },
    ));
    assert.equal(getStats().errorCount, before + 1);
  });

  // The health-budget contract: an account's errorCount (the signal that
  // eventually evicts it from rotation) must only move on genuine account
  // faults — not on a tier wall a client triggered by naming a paid selector.
  function errorCountOf(id) {
    return getAccountList().find((a) => a.id === id)?.errorCount ?? 0;
  }

  it('does NOT penalize the account error budget on MODEL_BLOCKED (tier wall)', () => {
    const acct = seed('blocked');
    const before = errorCountOf(acct.id);
    // Free account asked for claude-* → upstream "/upgrade" → MODEL_BLOCKED.
    // That's an entitlement wall, not a bad token; the account stays healthy.
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'claude-opus-4.8', startTime: Date.now() - 5, err: Object.assign(new Error('/upgrade to access this model'), { code: 'MODEL_BLOCKED' }) },
    );
    assert.equal(errorCountOf(acct.id), before, 'MODEL_BLOCKED must not bump errorCount');
  });

  it('DOES penalize the account error budget on UNAUTHORIZED (bad token)', () => {
    const acct = seed('unauth');
    const before = errorCountOf(acct.id);
    finalizeConnectAccount(
      { id: acct.id, apiKey: acct.apiKey },
      { model: 'swe-1-6-slow', startTime: Date.now() - 5, err: Object.assign(new Error('bad token'), { code: 'UNAUTHORIZED' }) },
    );
    assert.equal(errorCountOf(acct.id), before + 1, 'UNAUTHORIZED is a real account fault');
  });
});