test: add payment/betting edge cases — concurrent bets, draw refunds, validation

17 tests covering: simultaneous bet placement, draw refund mechanics,
bet validation bounds, extreme ELO odds, Cashu token rejection, and
escrow lifecycle leak prevention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 10:17:55 +00:00
co-authored by Claude Opus 4.6
parent f15504b400
commit 27b3b89424
+188
View File
@@ -0,0 +1,188 @@
import { describe, it, expect, beforeEach } from 'vitest'
import {
placeBet,
settleBets,
getFightBets,
clearEscrow,
getPoolInfo,
} from './betting.js'
import { validateBet, calculateOdds, calculatePayout } from './odds.js'
beforeEach(() => {
clearEscrow()
})
describe('7.3 — simultaneous bets on same fight', () => {
it('two concurrent bets on same fight: both accepted into escrow', async () => {
const fightId = 'fight-sim-1'
const [bet1, bet2] = await Promise.all([
placeBet(fightId, 'pub1', 'botA', 500, 'cashuA_test_500', 1200, 1200, 'botA'),
placeBet(fightId, 'pub2', 'botB', 300, 'cashuA_test_300', 1200, 1200, 'botA'),
])
expect(bet1.fightId).toBe(fightId)
expect(bet2.fightId).toBe(fightId)
const bets = getFightBets(fightId)
expect(bets).toHaveLength(2)
const pool = getPoolInfo(fightId)!
expect(pool.totalPool).toBe(800) // 500 + 300
expect(pool.betCount).toBe(2)
})
it('settlement distributes correctly with multiple bets', async () => {
const fightId = 'fight-sim-2'
// Bet 1: pub1 bets on botA (winner)
await placeBet(fightId, 'pub1', 'botA', 1000, 'cashuA_test_1000', 1200, 1200, 'botA')
// Bet 2: pub2 bets on botB (loser)
await placeBet(fightId, 'pub2', 'botB', 500, 'cashuA_test_500', 1200, 1200, 'botA')
const settlements = await settleBets(fightId, 'botA')
expect(settlements).toHaveLength(2)
const winner = settlements.find(s => s.won)!
const loser = settlements.find(s => !s.won)!
expect(winner.payoutSats).toBeGreaterThan(0)
expect(winner.payoutToken).toBeTruthy()
expect(loser.payoutSats).toBe(0)
expect(loser.payoutToken).toBeNull()
})
})
describe('7.3 — fight cancelled/draw: bet refund', () => {
it('draw settlement refunds all bets', async () => {
const fightId = 'fight-draw-1'
await placeBet(fightId, 'pub1', 'botA', 1000, 'cashuA_test_1000', 1200, 1200, 'botA')
await placeBet(fightId, 'pub2', 'botB', 500, 'cashuA_test_500', 1200, 1200, 'botA')
// Draw: winnerId = null → all bets refunded
const settlements = await settleBets(fightId, null)
expect(settlements).toHaveLength(2)
for (const s of settlements) {
expect(s.won).toBe(false)
// Refund amount equals original bet amount
expect(s.payoutSats).toBeGreaterThan(0) // refund, not zero
expect(s.payoutToken).toBeTruthy() // minted refund token
}
// Verify bet 1 refunded correct amount
const bet1Settlement = settlements.find(s => s.payoutSats === 1000)
expect(bet1Settlement).toBeDefined()
// Escrow cleared after settlement
expect(getFightBets(fightId)).toHaveLength(0)
})
it('clearEscrow returns count of orphaned fight pools', async () => {
await placeBet('fight-a', 'pub1', 'botA', 500, 'cashuA_test', 1200, 1200, 'botA')
await placeBet('fight-b', 'pub2', 'botB', 300, 'cashuA_test', 1200, 1200, 'botA')
const cleared = clearEscrow()
expect(cleared).toBe(2)
// All pools gone
expect(getFightBets('fight-a')).toHaveLength(0)
expect(getFightBets('fight-b')).toHaveLength(0)
})
it('settlement on empty fight returns empty array', async () => {
const settlements = await settleBets('nonexistent-fight', 'botA')
expect(settlements).toEqual([])
})
})
describe('7.3 — bet validation edge cases', () => {
it('rejects zero-sat bet', () => {
const result = validateBet(0)
expect(result.valid).toBe(false)
expect(result.error).toBeDefined()
})
it('rejects negative bet', () => {
const result = validateBet(-100)
expect(result.valid).toBe(false)
})
it('rejects bet above maximum (100k sats)', () => {
const result = validateBet(100_001)
expect(result.valid).toBe(false)
})
it('accepts minimum valid bet (100 sats)', () => {
const result = validateBet(100)
expect(result.valid).toBe(true)
})
it('accepts maximum valid bet (100k sats)', () => {
const result = validateBet(100_000)
expect(result.valid).toBe(true)
})
})
describe('7.3 — odds at extreme ELO gaps', () => {
it('odds remain finite and positive at huge ELO gap', () => {
const odds = calculateOdds(3000, 100)
expect(odds.botAPayoutMultiplier).toBeGreaterThan(0)
expect(odds.botBPayoutMultiplier).toBeGreaterThan(0)
expect(isFinite(odds.botAPayoutMultiplier)).toBe(true)
expect(isFinite(odds.botBPayoutMultiplier)).toBe(true)
// Favorite should have lower payout multiplier
expect(odds.botAPayoutMultiplier).toBeLessThan(odds.botBPayoutMultiplier)
})
it('equal ELO produces near-even odds', () => {
const odds = calculateOdds(1200, 1200)
// Should be ~2.0x for both (minus house edge)
expect(odds.botAPayoutMultiplier).toBeCloseTo(odds.botBPayoutMultiplier, 1)
})
it('payout calculation is correct', () => {
const payout = calculatePayout(1000, 2.5)
expect(payout).toBe(2500)
})
})
describe('7.3 — Cashu token validation', () => {
it('placeBet rejects empty token', async () => {
await expect(
placeBet('fight-1', 'pub1', 'botA', 500, '', 1200, 1200, 'botA')
).rejects.toThrow(/Invalid.*Cashu/i)
})
it('placeBet rejects too-short token', async () => {
await expect(
placeBet('fight-1', 'pub1', 'botA', 500, 'short', 1200, 1200, 'botA')
).rejects.toThrow(/Invalid.*Cashu/i)
})
it('placeBet accepts valid cashuA-prefixed token', async () => {
const bet = await placeBet('fight-1', 'pub1', 'botA', 500, 'cashuA_valid_token_test', 1200, 1200, 'botA')
expect(bet.id).toBeTruthy()
expect(bet.amountSats).toBe(500)
})
})
describe('7.3 — wallet persistence (no server interaction)', () => {
it('escrow survives multiple bet/settle cycles without leaks', async () => {
// Cycle 1
await placeBet('f1', 'pub1', 'botA', 500, 'cashuA_test', 1200, 1200, 'botA')
await settleBets('f1', 'botA')
expect(getPoolInfo('f1')).toBeNull()
// Cycle 2
await placeBet('f2', 'pub1', 'botA', 300, 'cashuA_test', 1200, 1200, 'botA')
await settleBets('f2', null) // draw refund
expect(getPoolInfo('f2')).toBeNull()
// Cycle 3
await placeBet('f3', 'pub1', 'botA', 200, 'cashuA_test', 1200, 1200, 'botA')
await settleBets('f3', 'botB') // loss
expect(getPoolInfo('f3')).toBeNull()
// No orphaned escrow
expect(clearEscrow()).toBe(0)
})
})