// @ts-nocheck /** * Drift State Verifier - Position Verification Tests * * Tests for Bug #82 long-term fix: Comprehensive position identity verification * before attempting automatic close of orphaned positions. * * Created: Dec 10, 2025 */ import { describe, it, expect, beforeEach, jest } from '@jest/globals' import { getDriftStateVerifier } from '../../../lib/monitoring/drift-state-verifier' import { closePosition as importedClosePosition } from '../../../lib/drift/orders' // Mock dependencies const mockDriftService = { getPosition: jest.fn(), } const mockPrisma = { trade: { findUnique: jest.fn(), findMany: jest.fn(), findFirst: jest.fn(), update: jest.fn(), }, } const asMock = (fn: any) => fn as jest.Mock jest.mock('../../../lib/drift/client', () => ({ getDriftService: jest.fn(() => Promise.resolve(mockDriftService)), })) jest.mock('../../../lib/database/trades', () => ({ getPrismaClient: jest.fn(() => mockPrisma), })) jest.mock('../../../lib/drift/orders', () => ({ closePosition: jest.fn(), })) jest.mock('../../../lib/notifications/telegram', () => ({ sendTelegramMessage: jest.fn(), })) // Import DriftStateVerifier after mocks are set up // NOTE: Actual import will need to be added based on your export structure describe('Drift State Verifier - Position Verification', () => { let verifier: any const mockClosePosition = importedClosePosition as jest.Mock beforeEach(() => { // Reset all mocks before each test jest.clearAllMocks() verifier = getDriftStateVerifier() verifier.recentCloseAttempts = new Map() }) describe('CRITICAL: Active Position Protection', () => { it('should NOT close position when newer trade exists in database', async () => { // Scenario: User opened new position AFTER the DB record we're checking const oldTradeExitTime = new Date('2025-12-10T10:00:00Z') const newTradeCreatedTime = new Date('2025-12-10T10:15:00Z') asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'old-trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date('2025-12-10T09:00:00Z'), exitTime: oldTradeExitTime, exitReason: 'SL', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 15.45, // SOL tokens entryPrice: 142.5, // Different price = new position! unrealizedPnL: 45.2, side: 'long', }) // KEY: Database shows newer open trade asMock(mockPrisma.trade.findMany).mockResolvedValue([ { id: 'new-trade-456', createdAt: newTradeCreatedTime, }, ]) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // No recent cooldown // Simulate verification call await verifier.processMismatch({ tradeId: 'old-trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 15.45, dbExitReason: 'SL', timeSinceExit: 30 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should NOT close when entry price differs by >2%', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) // 20 min ago (past grace period) asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime, exitReason: 'TP1', positionSizeUSD: 8000, entryPrice: 140.0, // DB says entry at $140 configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 57.14, // Size matches perfectly entryPrice: 143.5, // But entry price 2.5% higher = different position! unrealizedPnL: 120.5, side: 'long', }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // No newer trades asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // Should skip due to entry price mismatch await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 57.14, dbExitReason: 'TP1', timeSinceExit: 20 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should NOT close when size differs by >15%', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime, exitReason: 'TP1', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 45.0, // 45 tokens vs expected 57.14 = 79% ratio (below 85% threshold) entryPrice: 140.0, unrealizedPnL: -25.5, side: 'long', }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 45.0, dbExitReason: 'TP1', timeSinceExit: 20 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should NOT close when direction differs', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', // DB says LONG entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime, exitReason: 'SL', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: -57.14, // Negative = SHORT position entryPrice: 140.0, unrealizedPnL: 80.0, side: 'short', // Drift shows SHORT }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: -57.14, dbExitReason: 'SL', timeSinceExit: 20 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should NOT close within 10-minute grace period', async () => { const exitTime = new Date(Date.now() - 5 * 60 * 1000) // Only 5 minutes ago asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 15 * 60 * 1000), exitTime, exitReason: 'TP2', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 57.14, entryPrice: 140.0, unrealizedPnL: 45.2, side: 'long', }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // Should skip due to grace period await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 57.14, dbExitReason: 'TP2', timeSinceExit: 5 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) }) describe('CRITICAL: Verified Ghost Closure', () => { it('should close when all verification checks pass', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) // 20 min ago (past grace period) asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'ghost-trade-789', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 40 * 60 * 1000), exitTime, exitReason: 'SL', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 57.14, // Size matches within 15% tolerance entryPrice: 140.2, // Price matches within 2% unrealizedPnL: -120.5, side: 'long', // Direction matches }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // No newer trades asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // No cooldown asMock(mockClosePosition).mockResolvedValue({ success: true, transactionSignature: '5YxABC123...', realizedPnL: -120.5, }) asMock(mockPrisma.trade.update).mockResolvedValue({}) // Simulate verification and close await verifier.processMismatch({ tradeId: 'ghost-trade-789', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 57.14, dbExitReason: 'SL', timeSinceExit: 20 * 60 * 1000, }) // Should have called closePosition with correct parameters expect(mockClosePosition).toHaveBeenCalledWith({ symbol: 'SOL-PERP', percentToClose: 100, slippageTolerance: 0.05, }) // Should have updated database with cleanup metadata expect(mockPrisma.trade.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'ghost-trade-789' }, }) ) }) it('should enforce 5-minute cooldown between attempts', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) const lastAttempt = Date.now() - 2 * 60 * 1000 // Only 2 minutes ago asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime, exitReason: 'SL', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 57.14, entryPrice: 140.0, unrealizedPnL: -45.2, side: 'long', }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // Database shows recent cleanup attempt asMock(mockPrisma.trade.findFirst).mockResolvedValue({ configSnapshot: { orphanCleanupTime: new Date(lastAttempt).toISOString(), }, }) // Should skip due to cooldown await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 57.14, dbExitReason: 'SL', timeSinceExit: 20 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) }) describe('CRITICAL: Edge Case Handling', () => { it('should handle missing database trade gracefully', async () => { asMock(mockPrisma.trade.findUnique).mockResolvedValue(null) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // Should not attempt close if DB record missing await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 0, dbExitReason: null, timeSinceExit: 0, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should handle Drift position already closed', async () => { asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime: new Date(Date.now() - 20 * 60 * 1000), exitReason: 'TP1', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // Position already closed on Drift asMock(mockDriftService.getPosition).mockResolvedValue(null) // Should not attempt close - already resolved await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 0, dbExitReason: 'TP1', timeSinceExit: 20 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should handle unknown market index gracefully', async () => { asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'UNKNOWN-PERP', // Invalid symbol direction: 'long', entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime: new Date(Date.now() - 20 * 60 * 1000), exitReason: 'SL', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // Should skip unknown markets await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'UNKNOWN-PERP', expectedState: 'closed', actualState: 'open', driftSize: 0, dbExitReason: 'SL', timeSinceExit: 20 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should log protection events to database', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 30 * 60 * 1000), exitTime, exitReason: 'TP1', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 57.14, entryPrice: 145.0, // 3.6% price difference = protection trigger unrealizedPnL: 180.0, side: 'long', }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) asMock(mockPrisma.trade.update).mockResolvedValue({}) // Should have logged protection event await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 57.14, dbExitReason: 'TP1', timeSinceExit: 20 * 60 * 1000, }) expect(mockPrisma.trade.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'trade-123' }, data: expect.objectContaining({ configSnapshot: expect.objectContaining({ path: ['protectionEvents'], }), }), }) ) }) }) describe('CRITICAL: Fail-Open Bias', () => { it('should NOT close when verification is uncertain', async () => { // Multiple ambiguous signals const exitTime = new Date(Date.now() - 15 * 60 * 1000) // Near grace period boundary asMock(mockPrisma.trade.findUnique).mockResolvedValue({ id: 'trade-123', symbol: 'SOL-PERP', direction: 'long', entryTime: new Date(Date.now() - 25 * 60 * 1000), exitTime, exitReason: 'SL', positionSizeUSD: 8000, entryPrice: 140.0, configSnapshot: {}, }) asMock(mockDriftService.getPosition).mockResolvedValue({ size: 55.0, // Size 96% of expected (within tolerance but marginal) entryPrice: 142.5, // Price 1.8% different (within tolerance but marginal) unrealizedPnL: 80.0, side: 'long', }) asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // No newer trades (but uncertain) asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // When signals are ambiguous, should err on side of NOT closing // (Better to miss cleanup than close active trade) await verifier.processMismatch({ tradeId: 'trade-123', symbol: 'SOL-PERP', expectedState: 'closed', actualState: 'open', driftSize: 55.0, dbExitReason: 'SL', timeSinceExit: 15 * 60 * 1000, }) expect(mockClosePosition).not.toHaveBeenCalled() }) }) })