/** * 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, afterEach, jest } from '@jest/globals' import type { Mock } from 'jest-mock' // Mock dependencies const mockDriftService = { getPosition: jest.fn(), } const mockPrisma = { trade: { findUnique: jest.fn(), findMany: jest.fn(), findFirst: jest.fn(), update: jest.fn(), }, } const mockClosePosition = jest.fn() const mockSendTelegramMessage = jest.fn() 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: mockClosePosition, })) jest.mock('../../../lib/notifications/telegram', () => ({ sendTelegramMessage: mockSendTelegramMessage, })) // 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', () => { beforeEach(() => { // Reset all mocks before each test jest.clearAllMocks() }) 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') 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: {}, }) 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 mockPrisma.trade.findMany.mockResolvedValue([ { id: 'new-trade-456', createdAt: newTradeCreatedTime, }, ]) mockPrisma.trade.findFirst.mockResolvedValue(null) // No recent cooldown // Simulate verification call // NOTE: Actual test implementation depends on your DriftStateVerifier structure 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) 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: {}, }) 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', }) mockPrisma.trade.findMany.mockResolvedValue([]) // No newer trades mockPrisma.trade.findFirst.mockResolvedValue(null) // Should skip due to entry price mismatch expect(mockClosePosition).not.toHaveBeenCalled() }) it('should NOT close when size differs by >15%', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) 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: {}, }) 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', }) mockPrisma.trade.findMany.mockResolvedValue([]) mockPrisma.trade.findFirst.mockResolvedValue(null) expect(mockClosePosition).not.toHaveBeenCalled() }) it('should NOT close when direction differs', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) 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: {}, }) mockDriftService.getPosition.mockResolvedValue({ size: -57.14, // Negative = SHORT position entryPrice: 140.0, unrealizedPnL: 80.0, side: 'short', // Drift shows SHORT }) mockPrisma.trade.findMany.mockResolvedValue([]) mockPrisma.trade.findFirst.mockResolvedValue(null) 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 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: {}, }) mockDriftService.getPosition.mockResolvedValue({ size: 57.14, entryPrice: 140.0, unrealizedPnL: 45.2, side: 'long', }) mockPrisma.trade.findMany.mockResolvedValue([]) mockPrisma.trade.findFirst.mockResolvedValue(null) // Should skip due to grace period 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) 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: {}, }) 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 }) mockPrisma.trade.findMany.mockResolvedValue([]) // No newer trades mockPrisma.trade.findFirst.mockResolvedValue(null) // No cooldown mockClosePosition.mockResolvedValue({ success: true, transactionSignature: '5YxABC123...', realizedPnL: -120.5, }) mockPrisma.trade.update.mockResolvedValue({}) // Simulate verification and close // NOTE: Actual test implementation depends on your DriftStateVerifier structure // 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 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: {}, }) mockDriftService.getPosition.mockResolvedValue({ size: 57.14, entryPrice: 140.0, unrealizedPnL: -45.2, side: 'long', }) mockPrisma.trade.findMany.mockResolvedValue([]) // Database shows recent cleanup attempt mockPrisma.trade.findFirst.mockResolvedValue({ configSnapshot: { orphanCleanupTime: new Date(lastAttempt).toISOString(), }, }) // Should skip due to cooldown expect(mockClosePosition).not.toHaveBeenCalled() }) }) describe('CRITICAL: Edge Case Handling', () => { it('should handle missing database trade gracefully', async () => { mockPrisma.trade.findUnique.mockResolvedValue(null) mockPrisma.trade.findFirst.mockResolvedValue(null) // Should not attempt close if DB record missing expect(mockClosePosition).not.toHaveBeenCalled() }) it('should handle Drift position already closed', async () => { 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: {}, }) mockPrisma.trade.findFirst.mockResolvedValue(null) // Position already closed on Drift mockDriftService.getPosition.mockResolvedValue(null) // Should not attempt close - already resolved expect(mockClosePosition).not.toHaveBeenCalled() }) it('should handle unknown market index gracefully', async () => { 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: {}, }) mockPrisma.trade.findFirst.mockResolvedValue(null) // Should skip unknown markets expect(mockClosePosition).not.toHaveBeenCalled() }) it('should log protection events to database', async () => { const exitTime = new Date(Date.now() - 20 * 60 * 1000) 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: {}, }) mockDriftService.getPosition.mockResolvedValue({ size: 57.14, entryPrice: 145.0, // 3.6% price difference = protection trigger unrealizedPnL: 180.0, side: 'long', }) mockPrisma.trade.findMany.mockResolvedValue([]) mockPrisma.trade.findFirst.mockResolvedValue(null) mockPrisma.trade.update.mockResolvedValue({}) // Should have logged protection event 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 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: {}, }) 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', }) mockPrisma.trade.findMany.mockResolvedValue([]) // No newer trades (but uncertain) mockPrisma.trade.findFirst.mockResolvedValue(null) // When signals are ambiguous, should err on side of NOT closing // (Better to miss cleanup than close active trade) expect(mockClosePosition).not.toHaveBeenCalled() }) }) })