release: opensource snapshot 2026-02-27 19:25:00
This commit is contained in:
125
tests/concurrency/billing/ledger.concurrency.test.ts
Normal file
125
tests/concurrency/billing/ledger.concurrency.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { calcText } from '@/lib/billing/cost'
|
||||
import {
|
||||
confirmChargeWithRecord,
|
||||
freezeBalance,
|
||||
getBalance,
|
||||
rollbackFreeze,
|
||||
} from '@/lib/billing/ledger'
|
||||
import { withTextBilling } from '@/lib/billing/service'
|
||||
import { prisma } from '../../helpers/prisma'
|
||||
import { resetBillingState } from '../../helpers/db-reset'
|
||||
import { createTestProject, createTestUser, seedBalance } from '../../helpers/billing-fixtures'
|
||||
import { expectNoNegativeLedger } from '../../helpers/assertions'
|
||||
|
||||
describe('billing/concurrency', () => {
|
||||
beforeEach(async () => {
|
||||
await resetBillingState()
|
||||
process.env.BILLING_MODE = 'ENFORCE'
|
||||
})
|
||||
|
||||
it('does not create negative balance during high-concurrency freezes', async () => {
|
||||
const user = await createTestUser()
|
||||
await seedBalance(user.id, 10)
|
||||
|
||||
const attempts = Array.from({ length: 40 }, (_, idx) =>
|
||||
freezeBalance(user.id, 1, { idempotencyKey: `concurrency_freeze_${idx}` }))
|
||||
const freezeIds = await Promise.all(attempts)
|
||||
const successCount = freezeIds.filter(Boolean).length
|
||||
|
||||
const balance = await getBalance(user.id)
|
||||
expect(successCount).toBeLessThanOrEqual(10)
|
||||
expect(balance.balance).toBeCloseTo(10 - successCount, 8)
|
||||
expect(balance.frozenAmount).toBeCloseTo(successCount, 8)
|
||||
await expectNoNegativeLedger(user.id)
|
||||
})
|
||||
|
||||
it('applies idempotency key correctly under concurrent duplicate requests', async () => {
|
||||
const user = await createTestUser()
|
||||
await seedBalance(user.id, 10)
|
||||
|
||||
const attempts = Array.from({ length: 20 }, () =>
|
||||
freezeBalance(user.id, 2, { idempotencyKey: 'same_key_concurrency' }))
|
||||
const freezeIds = await Promise.all(attempts)
|
||||
const uniqueIds = new Set(freezeIds.filter(Boolean))
|
||||
|
||||
expect(uniqueIds.size).toBe(1)
|
||||
const balance = await getBalance(user.id)
|
||||
expect(balance.balance).toBeCloseTo(8, 8)
|
||||
expect(balance.frozenAmount).toBeCloseTo(2, 8)
|
||||
expect(await prisma.balanceFreeze.count()).toBe(1)
|
||||
})
|
||||
|
||||
it('keeps a valid final state when confirm and rollback race', async () => {
|
||||
const user = await createTestUser()
|
||||
const project = await createTestProject(user.id)
|
||||
await seedBalance(user.id, 10)
|
||||
|
||||
const freezeId = await freezeBalance(user.id, 5, { idempotencyKey: 'race_key' })
|
||||
expect(freezeId).toBeTruthy()
|
||||
|
||||
const [confirmResult, rollbackResult] = await Promise.allSettled([
|
||||
confirmChargeWithRecord(
|
||||
freezeId!,
|
||||
{
|
||||
projectId: project.id,
|
||||
action: 'race_confirm',
|
||||
apiType: 'text',
|
||||
model: 'anthropic/claude-sonnet-4',
|
||||
quantity: 10,
|
||||
unit: 'token',
|
||||
},
|
||||
{ chargedAmount: 3 },
|
||||
),
|
||||
rollbackFreeze(freezeId!),
|
||||
])
|
||||
|
||||
expect(['fulfilled', 'rejected']).toContain(confirmResult.status)
|
||||
expect(['fulfilled', 'rejected']).toContain(rollbackResult.status)
|
||||
expect(confirmResult.status === 'fulfilled' || rollbackResult.status === 'fulfilled').toBe(true)
|
||||
|
||||
const freeze = await prisma.balanceFreeze.findUnique({ where: { id: freezeId! } })
|
||||
expect(['confirmed', 'rolled_back']).toContain(freeze?.status)
|
||||
|
||||
const balance = await getBalance(user.id)
|
||||
if (freeze?.status === 'confirmed') {
|
||||
expect(balance.balance).toBeCloseTo(7, 8)
|
||||
expect(balance.totalSpent).toBeCloseTo(3, 8)
|
||||
} else {
|
||||
expect(balance.balance).toBeCloseTo(10, 8)
|
||||
expect(balance.totalSpent).toBeCloseTo(0, 8)
|
||||
}
|
||||
expect(balance.frozenAmount).toBeCloseTo(0, 8)
|
||||
await expectNoNegativeLedger(user.id)
|
||||
})
|
||||
|
||||
it('prevents duplicate consumption on retried sync billing with same requestId', async () => {
|
||||
const user = await createTestUser()
|
||||
const project = await createTestProject(user.id)
|
||||
await seedBalance(user.id, 5)
|
||||
|
||||
const attempt = () =>
|
||||
withTextBilling(
|
||||
user.id,
|
||||
'anthropic/claude-sonnet-4',
|
||||
1000,
|
||||
500,
|
||||
{
|
||||
projectId: project.id,
|
||||
action: 'retry_no_double_charge',
|
||||
requestId: 'fixed_request_id',
|
||||
},
|
||||
async () => ({ ok: true }),
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled([attempt(), attempt(), attempt()])
|
||||
expect(results.some((item) => item.status === 'fulfilled')).toBe(true)
|
||||
|
||||
const balance = await getBalance(user.id)
|
||||
const expected = calcText('anthropic/claude-sonnet-4', 1000, 500)
|
||||
expect(balance.totalSpent).toBeLessThanOrEqual(expected + 1e-8)
|
||||
expect(await prisma.balanceFreeze.count()).toBe(1)
|
||||
expect(await prisma.balanceTransaction.count({ where: { type: 'consume' } })).toBeLessThanOrEqual(1)
|
||||
await expectNoNegativeLedger(user.id)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user