import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, type LimitedItem } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { timeout } from "@tests/utils/genUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import { Decimal } from "decimal.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const testCase = "concurrent-track7"; // Product with both lifetime and monthly Messages features const lifetimeMessagesItem = constructFeatureItem({ featureId: TestFeature.Messages, includedUsage: 20000, interval: null, // Lifetime }) as LimitedItem; const monthlyMessagesItem = constructFeatureItem({ featureId: TestFeature.Messages, includedUsage: 10000, interval: "month" as any, intervalCount: 1, }) as LimitedItem; const pro = constructProduct({ type: "free", isDefault: false, items: [lifetimeMessagesItem, monthlyMessagesItem], }); const NUM_REQUESTS = 500; // Reduced from 10000 to avoid DB parameter limits const NUM_CUSTOMERS = 3; // Calculate total included usage dynamically const TOTAL_INCLUDED_USAGE = (lifetimeMessagesItem.included_usage ?? 0) + (monthlyMessagesItem.included_usage ?? 0); // Helper to generate random decimal between min and max using Decimal.js const randomDecimal = (min: number, max: number): Decimal => { const value = Math.random() * (max - min) + min; return new Decimal(value).toDecimalPlaces(2); }; describe(`${chalk.yellowBright(`${testCase}: Stress test with 10k concurrent requests per customer through check (send_event)`)}`, () => { const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); const customerIds = Array.from( { length: NUM_CUSTOMERS }, (_, i) => `${testCase}_customer${i + 1}`, ); // Store expected total usage per customer using Decimal for precision const customerExpectedUsage: Record = {}; beforeAll(async () => { // Initialize all customers for (const customerId of customerIds) { await initCustomerV3({ ctx, customerId, withTestClock: false, }); } await initProductsV0({ ctx, products: [pro], prefix: testCase, }); for (const customerId of customerIds) { await autumnV1.attach({ customer_id: customerId, product_id: pro.id, }); // Initialize expected usage to 0 customerExpectedUsage[customerId] = new Decimal(0); } }); test("should have initial balances for all customers", async () => { for (const customerId of customerIds) { const customer = await autumnV1.customers.get(customerId); console.log(`\nšŸ” Initial state for ${customerId}:`); console.log( ` Balance: ${customer.features[TestFeature.Messages].balance}`, ); console.log(` Usage: ${customer.features[TestFeature.Messages].usage}`); // Total balance should be lifetime + monthly expect(customer.features[TestFeature.Messages].balance).toBe( TOTAL_INCLUDED_USAGE, ); expect(customer.features[TestFeature.Messages].usage).toBe(0); expect(customer.features[TestFeature.Messages].breakdown?.length).toBe(2); } }); test(`should handle ${NUM_REQUESTS * NUM_CUSTOMERS} concurrent requests across ${NUM_CUSTOMERS} customers`, async () => { console.log( `\nšŸš€ Starting ${NUM_REQUESTS * NUM_CUSTOMERS} concurrent track requests...`, ); console.log( ` ${NUM_REQUESTS} requests per customer Ɨ ${NUM_CUSTOMERS} customers`, ); const allPromises: Promise[] = []; // Generate requests for each customer for (const customerId of customerIds) { const customerPromises: Promise[] = []; for (let i = 0; i < NUM_REQUESTS; i++) { // Generate random value between 0.01 and 2.00 using Decimal const decimalValue = randomDecimal(0.01, 2.0); const value = decimalValue.toDecimalPlaces(5).toNumber(); // Accumulate expected usage using Decimal for precision customerExpectedUsage[customerId] = customerExpectedUsage[customerId].plus(decimalValue); // Create track request for Messages feature with timing const requestStart = Date.now(); const promise = autumnV1 .check({ customer_id: customerId, feature_id: TestFeature.Messages, send_event: true, required_balance: value, skip_event: true, // Skip event insertion for stress test }) .then(() => Date.now() - requestStart); customerPromises.push(promise); } allPromises.push(...customerPromises); } // Execute all requests concurrently const startTime = Date.now(); const durations = await Promise.all(allPromises); const endTime = Date.now(); // Calculate P99 const sortedDurations = durations.sort((a, b) => a - b); const p99Index = Math.floor(sortedDurations.length * 0.99); const p99 = sortedDurations[p99Index]; console.log( `\nāœ… Completed ${NUM_REQUESTS * NUM_CUSTOMERS} requests in ${endTime - startTime}ms`, ); console.log( ` Average: ${((endTime - startTime) / (NUM_REQUESTS * NUM_CUSTOMERS)).toFixed(2)}ms per request`, ); console.log(` P99: ${p99.toFixed(2)}ms`); // Log expected totals per customer for (const customerId of customerIds) { console.log(`\nšŸ“Š ${customerId}:`); console.log( ` Total usage: ${customerExpectedUsage[customerId].toFixed(2)} units`, ); } }); test("should have correct cached balances for all customers", async () => { for (const customerId of customerIds) { const customer = await autumnV1.customers.get(customerId); const totalUsage = customerExpectedUsage[customerId]; // Balance should be capped at 0 (no negative balances without overage_allowed) const expectedBalance = Decimal.max( 0, new Decimal(TOTAL_INCLUDED_USAGE).minus(totalUsage), ) .toDP(5) .toNumber(); const actualBalance = new Decimal( customer.features[TestFeature.Messages].balance ?? 0, ) .toDP(5) .toNumber(); // Usage should be capped at included_usage without overage_allowed const expectedUsage = Decimal.min(totalUsage, TOTAL_INCLUDED_USAGE) .toDP(5) .toNumber(); const actualUsage = new Decimal( customer.features[TestFeature.Messages].usage ?? 0, ) .toDP(5) .toNumber(); // Verify balance and usage match expectations expect(actualBalance).toEqual(expectedBalance); expect(actualUsage).toEqual(expectedUsage); // Verify breakdown balances sum to top-level balance const breakdown = customer.features[TestFeature.Messages].breakdown; if (breakdown && breakdown.length > 0) { const breakdownBalance = breakdown.reduce( (sum, b) => new Decimal(sum).plus(b.balance || 0).toNumber(), 0, ); expect(new Decimal(breakdownBalance).toDP(5).toNumber()).toEqual( actualBalance!, ); } } }); test("should have correct non-cached balances for all customers after 2s", async () => { console.log("\nā³ Waiting 2s for DB sync..."); await timeout(5000); for (const customerId of customerIds) { const customer = await autumnV1.customers.get(customerId, { skip_cache: "true", }); const totalUsage = customerExpectedUsage[customerId]; // Balance should be capped at 0 (no negative balances without overage_allowed) const expectedBalance = Decimal.max( 0, new Decimal(TOTAL_INCLUDED_USAGE).minus(totalUsage), ) .toDP(5) .toNumber(); const actualBalance = new Decimal( customer.features[TestFeature.Messages].balance ?? 0, ) .toDP(5) .toNumber(); // Usage should be capped at included_usage without overage_allowed const expectedUsage = Decimal.min(totalUsage, TOTAL_INCLUDED_USAGE) .toDP(5) .toNumber(); const actualUsage = new Decimal( customer.features[TestFeature.Messages].usage ?? 0, ) .toDP(5) .toNumber(); // Use Decimal for precise comparisons - expect exact match expect(actualBalance).toEqual(expectedBalance); // Verify usage matches - expect exact match expect(actualUsage).toEqual(expectedUsage); // Verify breakdown balances match top-level (lifetime + monthly) const breakdown = customer.features[TestFeature.Messages].breakdown; if (breakdown && breakdown.length > 0) { const breakdownBalance = breakdown.reduce( (sum, b) => new Decimal(sum).plus(b.balance || 0).toNumber(), 0, ); expect(new Decimal(breakdownBalance).toDP(5).toNumber()).toEqual( actualBalance!, ); } } console.log("\nāœ… All balances verified successfully!"); }); });