/** * CRITICAL TEST: Position Manager Actually Monitors * * This test validates that Position Manager doesn't just say "added" but ACTUALLY * starts monitoring positions. This bug caused $1,000+ in losses. * * Bug: Position Manager logs "✅ Trade added" but never actually monitors * Impact: No TP/SL monitoring, no protection, uncontrolled losses * * Created: Dec 8, 2025 * Reason: User lost $1,000 because Position Manager never monitored despite logs claiming it did */ import { PositionManager } from '../../../lib/trading/position-manager' import { ActiveTrade } from '../../../lib/trading/position-manager' import { createMockTrade } from '../../helpers/trade-factory' // Mock dependencies const mockDriftService = { isInitialized: true, getPosition: jest.fn().mockResolvedValue({ size: 0 }), getClient: jest.fn(() => ({})), } jest.mock('../../../lib/drift/client', () => ({ getDriftService: jest.fn(() => mockDriftService), })) jest.mock('../../../lib/drift/orders', () => ({ placeExitOrders: jest.fn(async () => ({ success: true, signatures: [], expectedOrders: 0, placedOrders: 0, })), closePosition: jest.fn(async () => ({ success: true })), cancelAllOrders: jest.fn(async () => ({ success: true })), })) jest.mock('../../../lib/pyth/price-monitor') jest.mock('../../../lib/database/trades') jest.mock('../../../lib/notifications/telegram') jest.mock('../../../lib/utils/persistent-logger', () => ({ logCriticalError: jest.fn(), logError: jest.fn(), logWarning: jest.fn(), })) describe('Position Manager Monitoring Verification', () => { let manager: PositionManager let mockPriceMonitor: any beforeEach(() => { jest.clearAllMocks() mockDriftService.getPosition.mockResolvedValue({ size: 1 }) // Mock Pyth price monitor mockPriceMonitor = { start: jest.fn().mockResolvedValue(undefined), stop: jest.fn().mockResolvedValue(undefined), getLatestPrice: jest.fn().mockResolvedValue(140.00) } const { getPythPriceMonitor } = require('../../../lib/pyth/price-monitor') getPythPriceMonitor.mockReturnValue(mockPriceMonitor) manager = new PositionManager() }) afterEach(async () => { if ((manager as any)?.isMonitoring) { await (manager as any).stopMonitoring?.() } }) describe('CRITICAL: Monitoring Actually Starts', () => { it('should start Pyth price monitor when trade added', async () => { const trade = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' }) await manager.addTrade(trade) // CRITICAL: Verify Pyth monitor.start() was actually called expect(mockPriceMonitor.start).toHaveBeenCalledTimes(1) expect(mockPriceMonitor.start).toHaveBeenCalledWith( expect.objectContaining({ symbols: ['SOL-PERP'], onPriceUpdate: expect.any(Function), onError: expect.any(Function) }) ) }) it('should set isMonitoring flag to true after starting', async () => { const trade = createMockTrade({ direction: 'long' }) await manager.addTrade(trade) // Access private property via type assertion for testing const monitoring = (manager as any).isMonitoring expect(monitoring).toBe(true) }) it('should NOT start monitoring twice if already active', async () => { const trade1 = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' }) const trade2 = createMockTrade({ direction: 'long', symbol: 'SOL-PERP', id: 'trade2' }) await manager.addTrade(trade1) await manager.addTrade(trade2) // Should only call start() once (not twice) expect(mockPriceMonitor.start).toHaveBeenCalledTimes(1) }) it('should track multiple symbols in single monitoring session', async () => { const solTrade = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' }) const ethTrade = createMockTrade({ direction: 'long', symbol: 'ETH-PERP', id: 'eth-trade' }) await manager.addTrade(solTrade) // Start should be called first time expect(mockPriceMonitor.start).toHaveBeenCalledTimes(1) expect(mockPriceMonitor.start).toHaveBeenCalledWith( expect.objectContaining({ symbols: ['SOL-PERP'] }) ) // Adding second symbol should NOT restart monitor (optimization) // Existing monitor handles all symbols dynamically mockPriceMonitor.start.mockClear() await manager.addTrade(ethTrade) // Should NOT call start again - monitor already running expect(mockPriceMonitor.start).toHaveBeenCalledTimes(0) // Both trades should be tracked const activeTrades = (manager as any).activeTrades expect(activeTrades.size).toBe(2) expect(activeTrades.has(solTrade.id)).toBe(true) expect(activeTrades.has(ethTrade.id)).toBe(true) }) }) describe('CRITICAL: Price Updates Actually Trigger Checks', () => { it('should call price update handler when Pyth sends updates', async () => { const trade = createMockTrade({ direction: 'long', symbol: 'SOL-PERP', entryPrice: 140.00, tp1Price: 141.20 }) await manager.addTrade(trade) // Get the onPriceUpdate callback that was registered const startCall = mockPriceMonitor.start.mock.calls[0][0] const onPriceUpdate = startCall.onPriceUpdate expect(onPriceUpdate).toBeDefined() // Simulate price update await onPriceUpdate({ symbol: 'SOL-PERP', price: 141.25, timestamp: Date.now() }) // Trade should have updated lastPrice const activeTrade = (manager as any).activeTrades.get(trade.id) expect(activeTrade.lastPrice).toBe(141.25) expect(activeTrade.priceCheckCount).toBeGreaterThan(0) }) it('should update lastUpdateTime on every price check', async () => { const trade = createMockTrade({ direction: 'long' }) await manager.addTrade(trade) const startCall = mockPriceMonitor.start.mock.calls[0][0] const onPriceUpdate = startCall.onPriceUpdate const before = Date.now() await onPriceUpdate({ symbol: 'SOL-PERP', price: 140.50, timestamp: Date.now() }) const after = Date.now() const activeTrade = (manager as any).activeTrades.get(trade.id) expect(activeTrade.lastUpdateTime).toBeGreaterThanOrEqual(before) expect(activeTrade.lastUpdateTime).toBeLessThanOrEqual(after) }) }) describe('CRITICAL: Monitoring Stops When No Trades', () => { it('should stop monitoring when last trade removed', async () => { const trade = createMockTrade({ direction: 'long' }) await manager.addTrade(trade) expect(mockPriceMonitor.start).toHaveBeenCalled() await manager.removeTrade(trade.id) expect(mockPriceMonitor.stop).toHaveBeenCalledTimes(1) const monitoring = (manager as any).isMonitoring expect(monitoring).toBe(false) }) it('should NOT stop monitoring if other trades still active', async () => { const trade1 = createMockTrade({ direction: 'long', id: 'trade1' }) const trade2 = createMockTrade({ direction: 'long', id: 'trade2' }) await manager.addTrade(trade1) await manager.addTrade(trade2) await manager.removeTrade(trade1.id) // Should NOT have stopped (trade2 still active) expect(mockPriceMonitor.stop).not.toHaveBeenCalled() const monitoring = (manager as any).isMonitoring expect(monitoring).toBe(true) }) }) describe('CRITICAL: Error Handling Doesnt Break Monitoring', () => { it('should continue monitoring other trades if one trade errors', async () => { const trade1 = createMockTrade({ direction: 'long', id: 'trade1', symbol: 'SOL-PERP' }) const trade2 = createMockTrade({ direction: 'long', id: 'trade2', symbol: 'SOL-PERP' }) await manager.addTrade(trade1) await manager.addTrade(trade2) const startCall = mockPriceMonitor.start.mock.calls[0][0] const onPriceUpdate = startCall.onPriceUpdate // Should not throw - errors should be caught internally await expect(onPriceUpdate({ symbol: 'SOL-PERP', price: 141.00, timestamp: Date.now() })).resolves.not.toThrow() // Both trades should still be tracked const activeTrades = (manager as any).activeTrades expect(activeTrades.size).toBe(2) expect(activeTrades.has('trade1')).toBe(true) expect(activeTrades.has('trade2')).toBe(true) }) }) })