import { describe, it, expect, vi, beforeEach } from 'vitest' // Mock DB and dependencies to import bots module vi.mock('../db/index.js', () => ({ db: { select: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]), }), }), }), }, schema: { bots: { id: 'id', name: 'name', publicKey: 'publicKey', eloRating: 'eloRating', wins: 'wins', losses: 'losses', winStreak: 'winStreak', tier: 'tier', avatarSeed: 'avatarSeed', archetype: 'archetype', botType: 'botType', hasWallet: 'hasWallet', zapsReceived: 'zapsReceived', isActive: 'isActive', webhookUrl: 'webhookUrl', secretHash: 'secretHash', bestStreak: 'bestStreak', satsWagered: 'satsWagered' }, walletConnections: { id: 'id', botId: 'botId' }, }, })) vi.mock('../engine/scoring.js', () => ({ TIER_NAMES: ['Baby', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Legend'], TIER_COLORS: ['#999', '#cd7f32', '#c0c0c0', '#ffd700', '#e5e4e2', '#b9f2ff', '#ff6b6b'], })) vi.mock('../engine/achievements.js', () => ({ computeAchievements: vi.fn().mockReturnValue([]), })) vi.mock('../engine/orchestrator.js', () => ({ isAllowedWebhookUrl: vi.fn().mockReturnValue(true), })) vi.mock('../engine/webhook-test.js', () => ({ testWebhook: vi.fn().mockResolvedValue({ success: true }), })) vi.mock('../middleware/rate-limit.js', () => ({ rateLimit: () => async (_c: any, next: any) => next(), })) const { invalidateLeaderboardCache, _leaderboardCache } = await import('./bots.js') describe('leaderboard cache selective invalidation', () => { beforeEach(() => { _leaderboardCache.clear() }) it('invalidates __alltime__ and current but preserves historical season', () => { const now = Date.now() _leaderboardCache.set('__alltime__', { data: 'alltime', expiresAt: now + 30_000 }) _leaderboardCache.set('current', { data: 'current-season', expiresAt: now + 30_000 }) _leaderboardCache.set('s1', { data: 'season-1-history', expiresAt: now + 30_000 }) _leaderboardCache.set('s2', { data: 'season-2-history', expiresAt: now + 30_000 }) invalidateLeaderboardCache() expect(_leaderboardCache.has('__alltime__')).toBe(false) expect(_leaderboardCache.has('current')).toBe(false) // Historical seasons preserved expect(_leaderboardCache.has('s1')).toBe(true) expect(_leaderboardCache.has('s2')).toBe(true) }) it('also invalidates specific season when seasonId passed', () => { const now = Date.now() _leaderboardCache.set('__alltime__', { data: 'alltime', expiresAt: now + 30_000 }) _leaderboardCache.set('current', { data: 'current', expiresAt: now + 30_000 }) _leaderboardCache.set('s1', { data: 'season-1', expiresAt: now + 30_000 }) _leaderboardCache.set('s2', { data: 'season-2', expiresAt: now + 30_000 }) invalidateLeaderboardCache('s1') expect(_leaderboardCache.has('__alltime__')).toBe(false) expect(_leaderboardCache.has('current')).toBe(false) expect(_leaderboardCache.has('s1')).toBe(false) // Other seasons preserved expect(_leaderboardCache.has('s2')).toBe(true) }) it('no-op on empty cache', () => { expect(_leaderboardCache.size).toBe(0) invalidateLeaderboardCache() expect(_leaderboardCache.size).toBe(0) }) it('only removes targeted keys, leaves other entries', () => { const now = Date.now() // 5 cached entries _leaderboardCache.set('__alltime__', { data: 'a', expiresAt: now + 30_000 }) _leaderboardCache.set('current', { data: 'c', expiresAt: now + 30_000 }) _leaderboardCache.set('s1', { data: '1', expiresAt: now + 30_000 }) _leaderboardCache.set('s2', { data: '2', expiresAt: now + 30_000 }) _leaderboardCache.set('s3', { data: '3', expiresAt: now + 30_000 }) invalidateLeaderboardCache('s2') // Removed: __alltime__, current, s2 expect(_leaderboardCache.size).toBe(2) expect(_leaderboardCache.has('s1')).toBe(true) expect(_leaderboardCache.has('s3')).toBe(true) }) })