Ctrl K
ComunidadGuías…

test/billing-sync.test.js

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

Archivo original ↗Preguntar a KiBot
// Billing sync: cost tier + promo + credit rate from GetCascadeModelConfigs.
//
// The cost-tier half was already decoded, but three things were missing and each
// one is pinned below:
//
//   1. The RATE (`creditMultiplier`) on the same rows was thrown away, so
//      "free by a zero rate" could only come from GetUserStatus — which a free
//      account may never receive.
//   2. Nothing refreshed the billing snapshot after the first catalog sync
//      (`_modelCatalogSyncedKeys` latches per API key), so a promotion that
//      STARTED later was invisible forever.
//   3. No freshness stamp existed, so a FREE badge built from a stopped sync was
//      indistinguishable from one refreshed a minute ago.
//
// The clock is always injected: a promo end and a staleness budget are both
// time-dependent, and a test that reads the wall clock is a flake waiting for a
// calendar boundary.

import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  clearCloudModelCatalogs,
  getCloudBillingSyncState,
  getCloudCostTierInfo,
  getCloudCreditRate,
  getCloudSelectorPricing,
  getCloudZeroRateSelectors,
  mergeCloudCostTiers,
} from '../src/models.js';
import {
  __resetBillingSyncState,
  __resetModelCatalogState,
  __setModelCatalogDeps,
  addAccountByKey,
  getAccountInternal,
  getConnectFreeSelectors,
  getConnectPricingMap,
  getConnectSelectorPricing,
  getCurrentlyFreeConnectSelectors,
  refreshAccountBilling,
  refreshAllBilling,
  removeAccount,
  trySyncModelCatalog,
  __waitForModelCatalogSync,
} from '../src/auth.js';

const PROMO_END = '2099-12-31T23:59:59.999999999Z';
const PROMO_END_MS = Date.parse(PROMO_END);

function config(uid, { tier = null, promo = undefined, credit = 1 } = {}) {
  return {
    modelUid: uid,
    provider: 'MODEL_PROVIDER_WINDSURF',
    creditMultiplier: credit,
    ...(tier ? { modelCostTier: tier } : {}),
    ...(promo === undefined ? {} : { promoStatus: promo }),
  };
}

const created = [];
function seedAccount(credits = null) {
  const a = addAccountByKey(`sk-billing-sync-${Date.now()}-${Math.random().toString(36).slice(2)}`, 'billing-sync');
  created.push(a.id);
  const acct = getAccountInternal(a.id);
  acct.status = 'active';
  acct.tier = 'pro';
  acct.credits = credits;
  return acct;
}

let fetchCalls = 0;
let fetchResponse = { configs: [] };

/**
 * Adding an account fires trySyncModelCatalog() (fire-and-forget), which uses the
 * same injected GetCascadeModelConfigs. Wait for it and drop whatever it merged,
 * so a measured refresh below counts only its own RPC.
 */
async function settle() {
  await __waitForModelCatalogSync();
  clearCloudModelCatalogs();
  __resetBillingSyncState();
  fetchCalls = 0;
}

beforeEach(() => {
  fetchCalls = 0;
  fetchResponse = { configs: [] };
  __setModelCatalogDeps({
    disableConnectSync: true,
    getCascadeModelConfigs: async () => {
      fetchCalls++;
      return fetchResponse;
    },
  });
});

afterEach(async () => {
  for (const id of created.splice(0)) {
    try { removeAccount(id); } catch {}
  }
  __resetModelCatalogState();
  __resetBillingSyncState();
  __setModelCatalogDeps(null);
  clearCloudModelCatalogs();
});

describe('credit rate sync (creditMultiplier on the catalog row)', () => {
  it('has no rate data before any snapshot, and null is not "free"', () => {
    assert.equal(getCloudZeroRateSelectors(), null);
    assert.equal(getCloudCreditRate('swe-2-medium'), null);
    const pricing = getCloudSelectorPricing('swe-2-medium');
    assert.equal(pricing.freeNow, null, 'no live evidence must stay unknown');
    assert.equal(pricing.rate, null);
  });

  it('records the numeric rate per selector and treats a strict zero as free', () => {
    mergeCloudCostTiers([
      config('swe-2-medium', { credit: 0 }),
      config('swe-2-high', { credit: 9 }),
      config('swe-2-max', { credit: 0.5 }),
    ]);

    assert.deepEqual([...getCloudZeroRateSelectors()], ['swe-2-medium'],
      'a 0.5-credit row still bills and must not join the free set');
    assert.deepEqual(getCloudCreditRate('swe-2-high'), {
      credit: 9,
      fetchedAt: getCloudBillingSyncState().fetchedAt,
    });

    const free = getCloudSelectorPricing('swe-2-medium');
    assert.equal(free.freeNow, true);
    assert.equal(free.freeReason, 'rate_zero');
    assert.equal(free.rate, 0);

    const paid = getCloudSelectorPricing('swe-2-high');
    assert.equal(paid.freeNow, false, 'a known non-zero rate is a live "bills" answer');
    assert.equal(paid.rate, 9);
  });

  it('keeps the cost tier ahead of the rate as the free reason', () => {
    mergeCloudCostTiers([
      config('swe-2-medium', {
        tier: 'MODEL_COST_TIER_FREE',
        promo: { isActive: true, endDate: PROMO_END },
        credit: 6,
      }),
    ]);
    const pricing = getCloudSelectorPricing('swe-2-medium');
    assert.equal(pricing.freeNow, true);
    assert.equal(pricing.freeReason, 'cost_tier_free',
      'the tier is the upstream\'s own no-charge statement; the 6-credit rate is the list price');
    assert.equal(pricing.rate, 6);
    assert.equal(pricing.promoEndsAt, '2099-12-31T23:59:59.999Z');
  });

  it('does not let a response that omits the fields blank a known rate', () => {
    mergeCloudCostTiers([config('swe-2-medium', { credit: 0 })]);
    const before = getCloudBillingSyncState();
    // A partial body: rows exist, but no creditMultiplier and no cost tier.
    const merged = mergeCloudCostTiers([{ modelUid: 'swe-2-medium' }], { now: before.fetchedAt + 5_000 });
    assert.equal(merged.applied, false);
    assert.deepEqual([...getCloudZeroRateSelectors()], ['swe-2-medium']);
    assert.equal(getCloudBillingSyncState(before.fetchedAt + 5_000).fetchedAt, before.fetchedAt,
      'freshness must describe the rows actually held, not the last attempt');
  });

  it('replaces the map wholesale when the upstream stops marking a model free', () => {
    mergeCloudCostTiers([config('swe-2-medium', { tier: 'MODEL_COST_TIER_FREE', credit: 0 })]);
    assert.equal(getCloudSelectorPricing('swe-2-medium').freeNow, true);

    mergeCloudCostTiers([config('swe-2-medium', { tier: 'MODEL_COST_TIER_HIGH', credit: 6 })]);
    const after = getCloudSelectorPricing('swe-2-medium');
    assert.equal(after.freeNow, false, 'a stale promotion must not survive a fresh snapshot');
    assert.equal(after.costTier, 'MODEL_COST_TIER_HIGH');
  });
});

describe('freshness', () => {
  it('stamps the snapshot and reports staleness only after the budget', () => {
    const t0 = 1_800_000_000_000;
    mergeCloudCostTiers([config('swe-2-medium', { credit: 0 })], { now: t0 });
    const state = getCloudBillingSyncState(t0);
    assert.equal(state.fetchedAt, t0);
    assert.equal(state.stale, false);
    assert.equal(state.costTierRows, 0);
    assert.equal(state.rateRows, 1);

    const later = getCloudBillingSyncState(t0 + state.staleAfterMs + 1);
    assert.equal(later.stale, true, 'a stopped sync must become visible, not silently trusted');
  });

  it('surfaces the stamp on the pricing row the dashboard renders', () => {
    const t0 = 1_800_000_000_000;
    mergeCloudCostTiers([config('swe-2-medium', { credit: 0 })], { now: t0 });
    const pricing = getConnectSelectorPricing('swe-2-medium');
    assert.equal(pricing.pricingFetchedAt, t0);
    assert.equal(pricing.pricingStale, false);
    assert.equal(pricing.freeReason, 'rate_zero');
  });
});

describe('periodic refresh (refreshAccountBilling)', () => {
  it('pulls GetCascadeModelConfigs and applies the billing half', async () => {
    const account = seedAccount();
    await settle();
    fetchResponse = { configs: [config('swe-2-medium', { tier: 'MODEL_COST_TIER_FREE', credit: 6 })] };

    const result = await refreshAccountBilling(account.id, { now: 1_800_000_000_000 });
    assert.equal(result.ok, true);
    assert.equal(result.applied, true);
    assert.equal(result.costTierRows, 1);
    assert.equal(result.rateRows, 1);
    assert.equal(fetchCalls, 1);
    assert.equal(getCloudCostTierInfo('swe-2-medium').costTier, 'MODEL_COST_TIER_FREE');
  });

  it('honours the cadence and only re-fetches on force or after the interval', async () => {
    const account = seedAccount();
    await settle();
    const t0 = 1_800_000_000_000;
    fetchResponse = { configs: [config('swe-2-medium', { credit: 6 })] };

    await refreshAccountBilling(account.id, { now: t0 });
    assert.equal(fetchCalls, 1);

    const skipped = await refreshAccountBilling(account.id, { now: t0 + 60_000 });
    assert.equal(skipped.skipped, true);
    assert.equal(fetchCalls, 1, 'a second call inside the interval must not spend an RPC');

    await refreshAccountBilling(account.id, { now: t0 + 60_000, force: true });
    assert.equal(fetchCalls, 2);
  });

  it('skips inactive accounts and reports the whole pool', async () => {
    const active = seedAccount();
    const idle = addAccountByKey(`sk-billing-idle-${Date.now()}`, 'billing-idle');
    created.push(idle.id);
    getAccountInternal(idle.id).status = 'disabled';
    await settle();

    const results = await refreshAllBilling({ now: 1_800_000_000_000 });
    const forActive = results.find((r) => r.id === active.id);
    assert.ok(forActive, 'the active account is refreshed');
    assert.equal(results.some((r) => r.id === idle.id), false, 'a disabled account is not polled');
    assert.equal(fetchCalls, 1);
  });

  it('leaves the cadence stamp alone when the upstream call fails', async () => {
    const account = seedAccount();
    await settle();
    __setModelCatalogDeps({
      disableConnectSync: true,
      getCascadeModelConfigs: async () => { throw new Error('upstream 502'); },
    });

    const t0 = 1_800_000_000_000;
    const failed = await refreshAccountBilling(account.id, { now: t0 });
    assert.equal(failed.ok, false);

    // The next round retries immediately instead of waiting a full interval for a
    // one-second outage.
    fetchResponse = { configs: [config('swe-2-medium', { credit: 0 })] };
    __setModelCatalogDeps({
      disableConnectSync: true,
      getCascadeModelConfigs: async () => { fetchCalls++; return fetchResponse; },
    });
    const retried = await refreshAccountBilling(account.id, { now: t0 + 1_000 });
    assert.equal(retried.ok, true);
    assert.equal(fetchCalls, 1);
  });
});

describe('the catalog latch does not gate billing', () => {
  // _modelCatalogSyncedKeys exists so a repeated trySyncModelCatalog does not
  // re-spend the RPC. The billing refresh must NOT ride that latch (that is the
  // bug this feature fixes), and must not SET it either — otherwise a billing
  // refresh would convince the catalog sync it had already run.
  it('a second catalog sync stays latched while the billing refresh still fetches', async () => {
    const account = seedAccount();
    await settle();
    fetchResponse = { configs: [config('swe-2-medium', { credit: 6 })] };

    trySyncModelCatalog();
    await __waitForModelCatalogSync();
    assert.equal(fetchCalls, 0, 'the per-key latch still short-circuits a repeat catalog sync');

    const refreshed = await refreshAccountBilling(account.id, { force: true, now: 1_800_000_000_000 });
    assert.equal(refreshed.ok, true);
    assert.equal(fetchCalls, 1, 'the billing path is independent of the catalog latch');

    // And the billing refresh did not latch the catalog: a fresh key-less account
    // sync still runs (proved by the count going up once the key changes).
    getAccountInternal(account.id).apiKey = `${account.apiKey}-rotated`;
    trySyncModelCatalog();
    await __waitForModelCatalogSync();
    assert.equal(fetchCalls, 2, 'a rotated key is a new catalog identity, not a latched one');
  });
});

describe('pool-wide free list includes the synced rate', () => {
  it('marks a zero-rate selector free without any GetUserStatus rate table', () => {
    const account = seedAccount(null);
    mergeCloudCostTiers([config('swe-2-medium', { credit: 0 })], { accountId: account.id });

    const rateFree = getCurrentlyFreeConnectSelectors();
    assert.deepEqual([...rateFree], ['swe-2-medium']);
    assert.equal(getConnectSelectorPricing('swe-2-medium').freeNow, true);
    assert.equal(getConnectSelectorPricing('swe-2-medium').freeReason, 'rate_zero');
    assert.deepEqual(getConnectFreeSelectors(), ['swe-2-medium']);
  });

  it('bulk pricing agrees with the per-selector answer', () => {
    mergeCloudCostTiers([
      config('swe-2-medium', { tier: 'MODEL_COST_TIER_FREE', credit: 6 }),
      config('swe-2-high', { credit: 9 }),
    ]);
    const selectors = ['swe-2-medium', 'swe-2-high', 'swe-2-unknown'];
    const bulk = getConnectPricingMap(selectors);
    for (const selector of selectors) {
      assert.deepEqual(bulk[selector], getConnectSelectorPricing(selector),
        `${selector}: one snapshot of the inputs must produce one answer`);
    }
    assert.equal(bulk['swe-2-unknown'].freeNow, false,
      'live pricing data exists and is silent about this selector → bills');
  });

  it('a promo end in the past flips the free answer without another sync', () => {
    mergeCloudCostTiers([
      config('swe-2-medium', {
        tier: 'MODEL_COST_TIER_FREE',
        promo: { isActive: true, endDate: PROMO_END },
        credit: 6,
      }),
    ]);
    assert.equal(getCloudSelectorPricing('swe-2-medium', PROMO_END_MS - 1).freeNow, true);
    const expired = getCloudSelectorPricing('swe-2-medium', PROMO_END_MS + 1);
    assert.equal(expired.freeNow, false, 'an ended promo is over even before the next sync lands');
    assert.equal(expired.promoEndsAt, '2099-12-31T23:59:59.999Z',
      'the end date stays visible so the operator can see WHY it stopped');
  });
});