Ctrl K
ComunidadGuías…

test/billing-identity-rotation.test.js

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

Archivo original ↗Preguntar a KiBot
// Billing identity across a credential rotation (GPT-6-Pro review #3).
//
// The account ledger used to be keyed by `apiKey` — a secret that is allowed to
// rotate in place (background re-login, Firebase refresh, manual paste-over).
// A request holds the key it acquired with; if the pool re-keys the account
// while that request is in flight, `accounts.find(a => a.apiKey === snapshot)`
// no longer resolves and `recordAccountSpend` returns early. The turn finishes
// normally, the client sees [DONE], and the account ledger silently loses the
// row (requests/tokens/credit all under-report).
//
// The ledger must be owned by the immutable `accountId`. The credential epoch
// only decides whether a late FAILURE is evidence about the credential that is
// current now — it must never decide who owns a spend row.
//
// These are behaviour tests against the real handler / finalizer, not source
// greps: the connect stream is mocked at the `__setConnectDeps` seam, the
// rotation uses the real `setAccountTokens`, and the assertions read the real
// per-account ring (`getAccountRequests`) and lifetime tally (`getAccountPublic`).

import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  addAccountByKey,
  removeAccount,
  getAccountInternal,
  getAccountPublic,
  getAccountRequests,
  recordAccountSpend,
  setAccountTokens,
  __setReloginDeps,
  __resetReloginState,
} from '../src/auth.js';
import {
  handleChatCompletions,
  finalizeConnectAccount,
  __setConnectDeps,
  __resetConnectDeps,
} from '../src/handlers/chat.js';

const createdIds = [];
const prevEnv = {};

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

function fakeRes() {
  const listeners = new Map();
  return {
    body: '',
    writableEnded: false,
    write(chunk) { this.body += String(chunk); return true; },
    end(chunk) {
      if (chunk) this.write(chunk);
      this.writableEnded = true;
      for (const cb of listeners.get('close') || []) cb();
    },
    on(event, cb) {
      if (!listeners.has(event)) listeners.set(event, []);
      listeners.get(event).push(cb);
      return this;
    },
  };
}

function unauthorized() {
  return Object.assign(new Error('retired session token'), { code: 'UNAUTHORIZED' });
}

const spendOf = (id) => getAccountPublic(id)?.totalSpend || { requests: 0, totalTokens: 0 };

beforeEach(() => {
  prevEnv.DEVIN_CONNECT = process.env.DEVIN_CONNECT;
  prevEnv.AUTO_RELOGIN = process.env.DEVIN_CONNECT_AUTO_RELOGIN;
  process.env.DEVIN_CONNECT = '1';
  delete process.env.DEVIN_CONNECT_AUTO_RELOGIN;
});

afterEach(() => {
  __resetConnectDeps();
  __resetReloginState();
  __setReloginDeps(null);
  while (createdIds.length) removeAccount(createdIds.pop());
  if (prevEnv.DEVIN_CONNECT === undefined) delete process.env.DEVIN_CONNECT;
  else process.env.DEVIN_CONNECT = prevEnv.DEVIN_CONNECT;
  if (prevEnv.AUTO_RELOGIN === undefined) delete process.env.DEVIN_CONNECT_AUTO_RELOGIN;
  else process.env.DEVIN_CONNECT_AUTO_RELOGIN = prevEnv.AUTO_RELOGIN;
});

describe('review #3 — ledger ownership survives a mid-request key rotation', () => {
  it('a streaming turn that finishes after the key rotated still bills the account', async () => {
    const acct = seed('stream-rotate');
    const acquireKey = acct.apiKey;
    const rotatedKey = `${acquireKey}$ROTATED`;
    let rotatedMidFlight = false;

    __setConnectDeps({
      streamChatCompletion: async (params, send) => {
        assert.equal(params.token, acquireKey, 'the attempt presents the acquire-time credential');
        // Exactly what a background re-login / manual paste-over does: replace
        // the account's key in place while the handler still holds the old one.
        assert.equal(setAccountTokens(acct.id, { apiKey: rotatedKey }), true);
        rotatedMidFlight = true;
        send({ id: 's', object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content: 'OK' }, finish_reason: null }] });
        send({ id: 's', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
        return { usage: { prompt_tokens: 0, completion_tokens: 12, total_tokens: 12 } };
      },
    });

    const result = await handleChatCompletions(
      { model: 'swe-1-6-slow', stream: true, messages: [{ role: 'user', content: 'hi' }] },
      { callerKey: '' },
    );
    const res = fakeRes();
    await result.handler(res);

    assert.equal(result.status, 200);
    assert.ok(rotatedMidFlight, 'precondition: the rotation happened while the stream was in flight');
    assert.equal(getAccountInternal(acct.id).apiKey, rotatedKey, 'precondition: the rotation landed');
    assert.ok(res.body.includes('[DONE]'), 'the client still got a normal terminal frame');

    const page = getAccountRequests(acct.id);
    assert.equal(page.total, 1, 'exactly one ledger row for the turn');
    assert.equal(page.summary.requests, 1);
    assert.equal(page.summary.totalTokens, 12, 'the row carries the upstream usage');
    assert.ok(page.items[0].attemptId, 'the row records the attempt that produced it');

    const spend = spendOf(acct.id);
    assert.equal(spend.requests, 1, 'the lifetime tally must count the turn');
    assert.equal(spend.totalTokens, 12);
  });

  it('a late 401 from the retired credential does not charge the new epoch', () => {
    const acct = seed('late-401');
    const oldKey = acct.apiKey;
    // A reservation is a value snapshot: id + the credential it presented.
    const snapshot = { id: acct.id, apiKey: oldKey };
    getAccountInternal(acct.id)._inflight = 1; // mimic acquire

    assert.equal(setAccountTokens(acct.id, { apiKey: `${oldKey}$NEW` }), true);
    finalizeConnectAccount(snapshot, {
      model: 'swe-1-6-slow',
      startTime: Date.now() - 5,
      err: unauthorized(),
    });

    const live = getAccountInternal(acct.id);
    assert.equal(live.errorCount || 0, 0, 'the retired credential\'s failure must not charge the new key');
    assert.equal(live.status, 'active', 'the new key stays in rotation');
    assert.equal(live._inflight, 0, 'the slot is still released');
  });

  it('control: the same 401 on the current credential still charges it', () => {
    const acct = seed('current-401');
    const snapshot = { id: acct.id, apiKey: acct.apiKey };
    getAccountInternal(acct.id)._inflight = 1;

    finalizeConnectAccount(snapshot, {
      model: 'swe-1-6-slow',
      startTime: Date.now() - 5,
      err: unauthorized(),
    });

    assert.equal(getAccountInternal(acct.id).errorCount, 1,
      'the epoch guard must be scoped to stale credentials, not suppress auth faults');
    assert.equal(getAccountInternal(acct.id)._inflight, 0);
  });

  it('a stale 401 does not trigger a re-login that would rotate the fresh key', async () => {
    const acct = seed('handler-rotate');
    const acquireKey = acct.apiKey;
    const rotatedKey = `${acquireKey}$ROTATED`;
    process.env.DEVIN_CONNECT_AUTO_RELOGIN = '1';

    let logins = 0;
    __setReloginDeps({
      isCredStoreEnabled: () => true,
      getCredential: () => 'stored-password',
      windsurfLogin: async () => {
        logins++;
        return { apiKey: `devin-session-token$third-${Math.random().toString(36).slice(2)}` };
      },
    });
    let calls = 0;
    __setConnectDeps({
      streamChatCompletion: async (params, send) => {
        calls++;
        if (calls === 1) {
          assert.equal(params.token, acquireKey, 'the first attempt presents the acquire-time credential');
          setAccountTokens(acct.id, { apiKey: rotatedKey });
          throw unauthorized();
        }
        // The failover hop re-acquires the same account under its live key —
        // recovery happens without minting a third credential.
        assert.equal(params.token, rotatedKey, 'the retry presents the live credential');
        send({ id: 's', object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content: 'OK' }, finish_reason: null }] });
        send({ id: 's', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
        return { usage: { total_tokens: 3 } };
      },
    });

    const result = await handleChatCompletions(
      { model: 'swe-1-6-slow', stream: true, messages: [{ role: 'user', content: 'hi' }] },
      { callerKey: '' },
    );
    const res = fakeRes();
    await result.handler(res);

    assert.equal(logins, 0, 'a failure from the retired credential must not re-login the account');
    assert.ok(calls >= 2, 'the request recovered on the live key');
    assert.ok(res.body.includes('[DONE]'), 'the client got a completed turn');
    const live = getAccountInternal(acct.id);
    assert.equal(live.apiKey, rotatedKey, 'the freshly rotated key was not overwritten');
    assert.equal(live.errorCount || 0, 0, 'the new epoch was not charged for the old key\'s 401');
  });
});

describe('review #3 — recordAccountSpend identity contract', () => {
  it('resolves by accountId even when the key snapshot is stale', () => {
    const acct = seed('unit-id');
    recordAccountSpend(
      { accountId: acct.id, apiKey: 'retired-key', attemptId: 'attempt-42' },
      { total_tokens: 7 },
    );

    const page = getAccountRequests(acct.id);
    assert.equal(page.total, 1);
    assert.equal(page.summary.totalTokens, 7);
    assert.equal(page.items[0].attemptId, 'attempt-42');
  });

  it('an unknown accountId does not fall back to a key that resolves elsewhere', () => {
    const acct = seed('recycled-key');
    recordAccountSpend(
      { accountId: 'missing-account', apiKey: acct.apiKey },
      { total_tokens: 9 },
    );
    assert.equal(getAccountRequests(acct.id).total, 0,
      'the id is authoritative; a key alone must not re-attribute a row whose id is unknown');
  });
});