Ctrl K
ComunidadGuías…

test/billing-userstatus-decode.test.js

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

Archivo original ↗Preguntar a KiBot
/**
 * Test suite for GetUserStatusResponse billing decode (decodeUserStatusFull).
 *
 * Wire spec (calibrated 2026-07-07, live Teams paid account):
 *   #1.13.16 = balance (varint, micro-dollar)
 *   #1.13.17 = billing period start (varint, epoch seconds)
 *   #1.13.18 = billing period end (varint, epoch seconds)
 *   #1.13.1.2 = plan name (string)
 *   #1.13.1.21 (repeated) = credit rate table (f32 fixed32)
 *
 * Tests: synthetic protobuf buffers (built with proto.js writers) asserting:
 *   - Balance + period decode correctly (units verified)
 *   - Rate table decodes (f32 readFloatLE)
 *   - Catalog pairing (rate indices → selectors)
 *   - Graceful degradation (missing fields → null, no throw)
 */

import { strict as assert } from 'node:assert';
import { test } from 'node:test';
import {
  writeMessageField,
  writeVarintField,
  writeStringField,
  writeFixed32Field,
} from '../src/proto.js';
import { decodeUserStatusFull } from '../src/devin-connect-catalog.js';

/** Helper: build a credit-rate entry (fixed32 f32) */
function buildRateEntry(credit) {
  const buf = Buffer.allocUnsafe(4);
  buf.writeFloatLE(credit, 0);
  return writeMessageField(21, writeFixed32Field(2, buf));
}

/** Helper: build #1.13.1 (plan detail) */
function buildPlanDetail({ name, rates = [] }) {
  const planFields = [
    writeStringField(2, name),
    ...rates.map(buildRateEntry),
  ];
  return writeMessageField(1, Buffer.concat(planFields));
}

/** Helper: build #1.13 (billing block) */
function buildBillingBlock({ balance, periodStart, periodEnd, plan }) {
  const billingFields = [];
  if (plan) billingFields.push(buildPlanDetail(plan));
  if (balance != null) billingFields.push(writeVarintField(16, balance));
  if (periodStart != null) billingFields.push(writeVarintField(17, periodStart));
  if (periodEnd != null) billingFields.push(writeVarintField(18, periodEnd));
  return writeMessageField(13, Buffer.concat(billingFields));
}

/** Helper: build full GetUserStatusResponse */
function buildUserStatusResponse({ billing }) {
  const field1 = writeMessageField(1, billing);
  return field1;
}

test('decodeUserStatusFull: paid account with full billing ledger', () => {
  const periodStart = Math.floor(Date.now() / 1000);
  const periodEnd = periodStart + 86400 * 5; // 5-day period
  const balance = 80_000_000; // $80 in micro-dollars

  const billing = buildBillingBlock({
    balance,
    periodStart,
    periodEnd,
    plan: {
      name: 'Teams',
      rates: [25, 50, 6, 1.5, 12],
    },
  });

  const response = buildUserStatusResponse({ billing });
  const result = decodeUserStatusFull(response);

  assert.equal(result.plan, 'teams');
  assert.equal(result.isPaid, true);
  assert.equal(result.balance, 80);          // 80000000 micro-usd / 1e6 = $80
  assert.equal(result.balanceUnit, 'usd');    // already converted to USD
  assert.equal(result.balanceMicro, 80000000); // raw micro-usd preserved
  assert.equal(result.periodStart, periodStart * 1000); // epoch ms (number)
  assert.equal(result.periodEnd, periodEnd * 1000);
  assert.ok(Array.isArray(result.rateTable));
  assert.equal(result.rateTable.length, 5);
  assert.equal(result.rateTable[0], 25);
  assert.equal(result.rateTable[1], 50);
  assert.equal(result.rateTable[2], 6);
  assert.equal(result.rateTable[3], 1.5);
  assert.equal(result.rateTable[4], 12);
});

test('decodeUserStatusFull: catalog pairing (rates → selectors)', () => {
  const billing = buildBillingBlock({
    balance: 100_000_000,
    periodStart: 1783411200,
    periodEnd: 1783843200,
    plan: {
      name: 'Teams',
      rates: [25, 50, 6],
    },
  });

  const response = buildUserStatusResponse({ billing });
  const catalog = [
    { selector: 'claude-opus-4-8-medium' },
    { selector: 'claude-5-fable-medium' },
    { selector: 'claude-sonnet-5-medium' },
  ];

  const result = decodeUserStatusFull(response, catalog);

  assert.equal(typeof result.rateTable, 'object');
  assert.equal(result.rateTable['claude-opus-4-8-medium'], 25);
  assert.equal(result.rateTable['claude-5-fable-medium'], 50);
  assert.equal(result.rateTable['claude-sonnet-5-medium'], 6);
});

test('decodeUserStatusFull: free account (minimal billing)', () => {
  const billing = buildBillingBlock({
    plan: { name: 'Free', rates: [] },
  });

  const response = buildUserStatusResponse({ billing });
  const result = decodeUserStatusFull(response);

  assert.equal(result.plan, 'free');
  assert.equal(result.isPaid, false);
  assert.equal(result.balance, null);
  assert.equal(result.balanceUnit, null);
  assert.equal(result.periodStart, null);
  assert.equal(result.periodEnd, null);
  assert.equal(result.rateTable, null);
});

test('decodeUserStatusFull: partial billing (balance only)', () => {
  const billing = buildBillingBlock({
    balance: 50_000_000,
    plan: { name: 'Pro', rates: [] },
  });

  const response = buildUserStatusResponse({ billing });
  const result = decodeUserStatusFull(response);

  assert.equal(result.plan, 'pro');
  assert.equal(result.isPaid, true);
  assert.equal(result.balance, 50);
  assert.equal(result.balanceUnit, 'usd');
  assert.equal(result.periodStart, null);
  assert.equal(result.periodEnd, null);
});

test('decodeUserStatusFull: graceful degrade on malformed rate entry', () => {
  // Build a rate table with one valid entry and one malformed (wrong field #99 instead of #2)
  const buf = Buffer.allocUnsafe(4);
  buf.writeFloatLE(999, 0);
  const malformedRate = writeMessageField(21, writeFixed32Field(99, buf)); // field #99, not #2

  const planFields = [
    writeStringField(2, 'Teams'),
    buildRateEntry(25), // valid
    malformedRate, // malformed: wrong field number
    buildRateEntry(50), // another valid
  ];
  const billing = writeMessageField(13, writeMessageField(1, Buffer.concat(planFields)));
  const response = buildUserStatusResponse({ billing });

  const result = decodeUserStatusFull(response);

  assert.equal(result.plan, 'teams');
  assert.ok(Array.isArray(result.rateTable));
  assert.equal(result.rateTable.length, 3);
  assert.equal(result.rateTable[0], 25);
  assert.equal(result.rateTable[1], null); // malformed → null (no field #2)
  assert.equal(result.rateTable[2], 50);
});

test('decodeUserStatusFull: empty response (no #1 field)', () => {
  const response = Buffer.alloc(0);
  const result = decodeUserStatusFull(response);

  assert.equal(result.plan, 'unknown');
  assert.equal(result.isPaid, false);
  assert.equal(result.balance, null);
  assert.equal(result.periodStart, null);
  assert.equal(result.periodEnd, null);
  assert.equal(result.rateTable, null);
});

test('decodeUserStatusFull: fallback plan name from #2.#2 (legacy path)', () => {
  // Build a response with plan name only in #2.#2 (no billing block in #1)
  // The #2 field is at the TOP level, not inside #1
  const field2Inner = writeStringField(2, 'Enterprise');
  const field2 = writeMessageField(2, field2Inner);
  const result = decodeUserStatusFull(field2);

  assert.equal(result.plan, 'enterprise');
  assert.equal(result.isPaid, true);
  assert.equal(result.balance, null); // no billing block
});

test('decodeUserStatusFull: rate table longer than catalog (excess ignored)', () => {
  const billing = buildBillingBlock({
    plan: {
      name: 'Teams',
      rates: [25, 50, 6, 1.5, 12],
    },
  });

  const response = buildUserStatusResponse({ billing });
  const catalog = [
    { selector: 'claude-opus-4-8-medium' },
    { selector: 'claude-5-fable-medium' },
    // only 2 models vs 5 rates
  ];

  const result = decodeUserStatusFull(response, catalog);

  assert.equal(Object.keys(result.rateTable).length, 2);
  assert.equal(result.rateTable['claude-opus-4-8-medium'], 25);
  assert.equal(result.rateTable['claude-5-fable-medium'], 50);
  // rates[2..4] are not paired (no catalog entries)
});

test('decodeUserStatusFull: catalog entry without selector (skipped in pairing)', () => {
  const billing = buildBillingBlock({
    plan: { name: 'Teams', rates: [25, 50, 6] },
  });

  const response = buildUserStatusResponse({ billing });
  const catalog = [
    { selector: 'claude-opus-4-8-medium' },
    { selector: null }, // malformed catalog entry
    { selector: 'claude-sonnet-5-medium' },
  ];

  const result = decodeUserStatusFull(response, catalog);

  assert.equal(Object.keys(result.rateTable).length, 2);
  assert.equal(result.rateTable['claude-opus-4-8-medium'], 25);
  assert.equal(result.rateTable['claude-sonnet-5-medium'], 6);
  // index 1 skipped (no selector)
});

test('decodeUserStatusFull: uint64 -1 balance is unknown, not $18,446,744,073,709.43', () => {
  // #1.13.16 is a uint64 on the wire; -1 (the common "unset/unlimited" sentinel)
  // arrives as 2^64-1. It must never be formatted as a real on-demand balance.
  const billing = buildBillingBlock({ balance: 18446744073709551615n });
  const result = decodeUserStatusFull(buildUserStatusResponse({ billing }));

  assert.equal(result.balance, null);
  assert.equal(result.balanceUnit, null);
  assert.equal(result.balanceMicro, undefined);
});

test('decodeUserStatusFull: a plausible balance still decodes after the sentinel guard', () => {
  const billing = buildBillingBlock({ balance: 1_500_000 }); // $1.50
  const result = decodeUserStatusFull(buildUserStatusResponse({ billing }));

  assert.equal(result.balance, 1.5);
  assert.equal(result.balanceMicro, 1_500_000);
});