From 271222fb3686fa69d3a323c48c0acc3b3018538a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:27:58 +0000 Subject: [PATCH] test: Add comprehensive tests for bugs #76, #78, #80 Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com> --- .../cooldown-enforcement.test.ts | 336 ++++++++++++++++++ .../orders/exit-orders-validation.test.ts | 183 ++++++++++ .../safe-orphan-removal.test.ts | 266 ++++++++++++++ 3 files changed, 785 insertions(+) create mode 100644 tests/integration/drift-state-verifier/cooldown-enforcement.test.ts create mode 100644 tests/integration/orders/exit-orders-validation.test.ts create mode 100644 tests/integration/position-manager/safe-orphan-removal.test.ts diff --git a/tests/integration/drift-state-verifier/cooldown-enforcement.test.ts b/tests/integration/drift-state-verifier/cooldown-enforcement.test.ts new file mode 100644 index 0000000..b16da04 --- /dev/null +++ b/tests/integration/drift-state-verifier/cooldown-enforcement.test.ts @@ -0,0 +1,336 @@ +/** + * Bug #80 Test: Retry Loop Cooldown Enforcement + * + * Tests that drift-state-verifier respects 5-minute cooldown and doesn't + * repeatedly retry closing positions, which strips protection. + * + * Created: Dec 9, 2025 + * Reason: Bug #80 - Retry loop doesn't enforce cooldown properly + */ + +import { getDriftStateVerifier } from '../../../lib/monitoring/drift-state-verifier' + +// Mock dependencies +jest.mock('../../../lib/drift/client') +jest.mock('../../../lib/drift/orders') +jest.mock('../../../lib/database/trades') +jest.mock('../../../lib/notifications/telegram') + +describe('Bug #80: Retry Loop Cooldown', () => { + let verifier: any + let mockClosePosition: jest.Mock + let mockPrisma: any + + beforeEach(() => { + jest.clearAllMocks() + + // Get verifier instance + verifier = getDriftStateVerifier() + + // Access private methods via type assertion for testing + verifier.recentCloseAttempts = new Map() + + // Mock Drift service + const { getDriftService } = require('../../../lib/drift/client') + getDriftService.mockResolvedValue({ + getPosition: jest.fn().mockResolvedValue({ size: 0, side: 'none' }) + }) + + // Mock closePosition + const ordersModule = require('../../../lib/drift/orders') + mockClosePosition = jest.fn().mockResolvedValue({ + success: true, + transactionSignature: 'CLOSE_TX', + realizedPnL: -10.50 + }) + ordersModule.closePosition = mockClosePosition + + // Mock Prisma + const { getPrismaClient } = require('../../../lib/database/trades') + mockPrisma = { + trade: { + findUnique: jest.fn(), + update: jest.fn() + } + } + getPrismaClient.mockReturnValue(mockPrisma) + }) + + describe('In-Memory Cooldown Map', () => { + it('should allow first close attempt', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + mockPrisma.trade.findUnique.mockResolvedValue({ + id: 'trade1', + configSnapshot: {} + }) + + await (verifier as any).retryClose(mismatch) + + // Should have called closePosition + expect(mockClosePosition).toHaveBeenCalledWith({ + symbol: 'SOL-PERP', + percentToClose: 100, + slippageTolerance: 0.05 + }) + + // Should have recorded attempt in map + expect(verifier.recentCloseAttempts.has('SOL-PERP')).toBe(true) + }) + + it('should block retry within 5-minute cooldown', async () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation() + + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + // Set recent attempt (2 minutes ago) + const twoMinutesAgo = Date.now() - (2 * 60 * 1000) + verifier.recentCloseAttempts.set('SOL-PERP', twoMinutesAgo) + + await (verifier as any).retryClose(mismatch) + + // Should NOT have called closePosition + expect(mockClosePosition).not.toHaveBeenCalled() + + // Should have logged cooldown message + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('COOLDOWN ACTIVE') + ) + + consoleSpy.mockRestore() + }) + + it('should allow retry after cooldown expires (> 5 minutes)', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + // Set old attempt (6 minutes ago - cooldown expired) + const sixMinutesAgo = Date.now() - (6 * 60 * 1000) + verifier.recentCloseAttempts.set('SOL-PERP', sixMinutesAgo) + + mockPrisma.trade.findUnique.mockResolvedValue({ + id: 'trade1', + configSnapshot: {} + }) + + await (verifier as any).retryClose(mismatch) + + // Should have called closePosition (cooldown expired) + expect(mockClosePosition).toHaveBeenCalled() + }) + + it('should log remaining cooldown time', async () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation() + + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + // Set recent attempt (90 seconds ago) + const ninetySecondsAgo = Date.now() - (90 * 1000) + verifier.recentCloseAttempts.set('SOL-PERP', ninetySecondsAgo) + + await (verifier as any).retryClose(mismatch) + + // Should log remaining wait time + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringMatching(/Must wait \d+s more/) + ) + + consoleSpy.mockRestore() + }) + + it('should log cooldown map state', async () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation() + + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + // Set recent attempts for multiple symbols + verifier.recentCloseAttempts.set('SOL-PERP', Date.now() - 30000) + verifier.recentCloseAttempts.set('ETH-PERP', Date.now() - 60000) + + await (verifier as any).retryClose(mismatch) + + // Should log map state + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Cooldown map state:') + ) + + consoleSpy.mockRestore() + }) + }) + + describe('Database Cooldown Persistence', () => { + it('should check database for cooldown even if map is empty', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + // Map is empty (simulates restart) + expect(verifier.recentCloseAttempts.size).toBe(0) + + // But database has recent attempt + const twoMinutesAgo = new Date(Date.now() - (2 * 60 * 1000)) + mockPrisma.trade.findUnique.mockResolvedValue({ + id: 'trade1', + configSnapshot: { + retryCloseTime: twoMinutesAgo.toISOString() + } + }) + + await (verifier as any).retryClose(mismatch) + + // Should NOT have called closePosition (DB cooldown active) + expect(mockClosePosition).not.toHaveBeenCalled() + }) + + it('should update in-memory map from database', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + const oneMinuteAgo = new Date(Date.now() - (1 * 60 * 1000)) + mockPrisma.trade.findUnique.mockResolvedValue({ + id: 'trade1', + configSnapshot: { + retryCloseTime: oneMinuteAgo.toISOString() + } + }) + + await (verifier as any).retryClose(mismatch) + + // Should have updated map from DB + expect(verifier.recentCloseAttempts.has('SOL-PERP')).toBe(true) + }) + + it('should persist attempt to database after successful close', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + mockPrisma.trade.findUnique.mockResolvedValue({ + id: 'trade1', + configSnapshot: {} + }) + + mockPrisma.trade.update.mockResolvedValue({}) + + await (verifier as any).retryClose(mismatch) + + // Should have updated database with retry time + expect(mockPrisma.trade.update).toHaveBeenCalledWith({ + where: { id: 'trade1' }, + data: expect.objectContaining({ + configSnapshot: expect.objectContaining({ + retryCloseAttempted: true, + retryCloseTime: expect.any(String) + }) + }) + }) + }) + }) + + describe('Error Handling', () => { + it('should record attempt even on close failure to prevent spam', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + mockPrisma.trade.findUnique.mockResolvedValue({ + id: 'trade1', + configSnapshot: {} + }) + + // Close fails + mockClosePosition.mockResolvedValue({ + success: false, + error: 'RPC timeout' + }) + + await (verifier as any).retryClose(mismatch) + + // Should still have recorded attempt + expect(verifier.recentCloseAttempts.has('SOL-PERP')).toBe(true) + }) + + it('should record attempt on exception to prevent rapid retries', async () => { + const mismatch = { + tradeId: 'trade1', + symbol: 'SOL-PERP', + expectedState: 'closed' as const, + actualState: 'open' as const, + driftSize: 14.47, + dbExitReason: 'TP1', + timeSinceExit: 60000 + } + + mockPrisma.trade.findUnique.mockRejectedValue(new Error('Database error')) + + await (verifier as any).retryClose(mismatch) + + // Should have recorded attempt even on error + expect(verifier.recentCloseAttempts.has('SOL-PERP')).toBe(true) + }) + }) +}) diff --git a/tests/integration/orders/exit-orders-validation.test.ts b/tests/integration/orders/exit-orders-validation.test.ts new file mode 100644 index 0000000..dc6471a --- /dev/null +++ b/tests/integration/orders/exit-orders-validation.test.ts @@ -0,0 +1,183 @@ +/** + * Bug #76 Test: Stop-Loss Placement Validation + * + * Tests that placeExitOrders returns failure when SL signature is missing + * and logs critical errors appropriately. + * + * Created: Dec 9, 2025 + * Reason: Bug #76 - placeExitOrders() can return SUCCESS with missing SL orders + */ + +import { placeExitOrders, PlaceExitOrdersOptions } from '../../../lib/drift/orders' + +// Mock dependencies +jest.mock('../../../lib/drift/client') +jest.mock('../../../lib/utils/logger') +jest.mock('../../../config/trading') + +describe('Bug #76: Exit Orders Validation', () => { + let mockDriftClient: any + + beforeEach(() => { + jest.clearAllMocks() + + // Mock Drift service and client + mockDriftClient = { + placePerpOrder: jest.fn() + } + + const { getDriftService } = require('../../../lib/drift/client') + getDriftService.mockReturnValue({ + getClient: () => mockDriftClient + }) + + // Mock trading config + const { getMarketConfig } = require('../../../config/trading') + getMarketConfig.mockReturnValue({ + driftMarketIndex: 0, + minOrderSize: 0.1 + }) + }) + + describe('Single Stop System', () => { + it('should return success when all 3 orders placed (TP1 + TP2 + SL)', async () => { + // Mock successful order placements + mockDriftClient.placePerpOrder + .mockResolvedValueOnce('TP1_SIG') // TP1 + .mockResolvedValueOnce('TP2_SIG') // TP2 + .mockResolvedValueOnce('SL_SIG') // SL + + const options: PlaceExitOrdersOptions = { + symbol: 'SOL-PERP', + positionSizeUSD: 8000, + entryPrice: 140.00, + tp1Price: 141.20, + tp2Price: 142.41, + stopLossPrice: 138.71, + tp1SizePercent: 75, + tp2SizePercent: 0, + direction: 'long', + useDualStops: false + } + + const result = await placeExitOrders(options) + + expect(result.success).toBe(true) + expect(result.signatures).toHaveLength(3) + expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SL_SIG']) + }) + + it('should return failure when SL placement fails', async () => { + // Mock TP1 and TP2 success, but SL fails + mockDriftClient.placePerpOrder + .mockResolvedValueOnce('TP1_SIG') // TP1 + .mockResolvedValueOnce('TP2_SIG') // TP2 + .mockRejectedValueOnce(new Error('Rate limit exceeded')) // SL fails + + const options: PlaceExitOrdersOptions = { + symbol: 'SOL-PERP', + positionSizeUSD: 8000, + entryPrice: 140.00, + tp1Price: 141.20, + tp2Price: 142.41, + stopLossPrice: 138.71, + tp1SizePercent: 75, + tp2SizePercent: 0, + direction: 'long', + useDualStops: false + } + + const result = await placeExitOrders(options) + + expect(result.success).toBe(false) + expect(result.error).toContain('Stop loss placement failed') + }) + }) + + describe('Dual Stop System', () => { + it('should return success when all 4 orders placed (TP1 + TP2 + Soft + Hard)', async () => { + // Mock successful order placements + mockDriftClient.placePerpOrder + .mockResolvedValueOnce('TP1_SIG') // TP1 + .mockResolvedValueOnce('TP2_SIG') // TP2 + .mockResolvedValueOnce('SOFT_SIG') // Soft Stop + .mockResolvedValueOnce('HARD_SIG') // Hard Stop + + const options: PlaceExitOrdersOptions = { + symbol: 'SOL-PERP', + positionSizeUSD: 8000, + entryPrice: 140.00, + tp1Price: 141.20, + tp2Price: 142.41, + stopLossPrice: 138.71, + tp1SizePercent: 75, + tp2SizePercent: 0, + direction: 'long', + useDualStops: true, + softStopPrice: 139.00, + hardStopPrice: 138.50 + } + + const result = await placeExitOrders(options) + + expect(result.success).toBe(true) + expect(result.signatures).toHaveLength(4) + expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SOFT_SIG', 'HARD_SIG']) + }) + + it('should return failure when soft stop fails', async () => { + mockDriftClient.placePerpOrder + .mockResolvedValueOnce('TP1_SIG') // TP1 + .mockResolvedValueOnce('TP2_SIG') // TP2 + .mockRejectedValueOnce(new Error('Soft stop failed')) // Soft Stop + + const options: PlaceExitOrdersOptions = { + symbol: 'SOL-PERP', + positionSizeUSD: 8000, + entryPrice: 140.00, + tp1Price: 141.20, + tp2Price: 142.41, + stopLossPrice: 138.71, + tp1SizePercent: 75, + tp2SizePercent: 0, + direction: 'long', + useDualStops: true, + softStopPrice: 139.00, + hardStopPrice: 138.50 + } + + const result = await placeExitOrders(options) + + expect(result.success).toBe(false) + expect(result.error).toContain('Soft stop placement failed') + }) + + it('should return failure when hard stop fails', async () => { + mockDriftClient.placePerpOrder + .mockResolvedValueOnce('TP1_SIG') // TP1 + .mockResolvedValueOnce('TP2_SIG') // TP2 + .mockResolvedValueOnce('SOFT_SIG') // Soft Stop + .mockRejectedValueOnce(new Error('Hard stop failed')) // Hard Stop + + const options: PlaceExitOrdersOptions = { + symbol: 'SOL-PERP', + positionSizeUSD: 8000, + entryPrice: 140.00, + tp1Price: 141.20, + tp2Price: 142.41, + stopLossPrice: 138.71, + tp1SizePercent: 75, + tp2SizePercent: 0, + direction: 'long', + useDualStops: true, + softStopPrice: 139.00, + hardStopPrice: 138.50 + } + + const result = await placeExitOrders(options) + + expect(result.success).toBe(false) + expect(result.error).toContain('Hard stop placement failed') + }) + }) +}) diff --git a/tests/integration/position-manager/safe-orphan-removal.test.ts b/tests/integration/position-manager/safe-orphan-removal.test.ts new file mode 100644 index 0000000..464c0ac --- /dev/null +++ b/tests/integration/position-manager/safe-orphan-removal.test.ts @@ -0,0 +1,266 @@ +/** + * Bug #78 Test: Safe Orphan Removal + * + * Tests that removeTrade() checks Drift position size before canceling orders + * to avoid removing active position protection. + * + * Created: Dec 9, 2025 + * Reason: Bug #78 - cancelAllOrders affects ALL positions on symbol + */ + +import { PositionManager } from '../../../lib/trading/position-manager' +import { createMockTrade } from '../../helpers/trade-factory' + +// Mock dependencies +jest.mock('../../../lib/drift/client') +jest.mock('../../../lib/drift/orders') +jest.mock('../../../lib/pyth/price-monitor') +jest.mock('../../../lib/database/trades') +jest.mock('../../../lib/notifications/telegram') +jest.mock('../../../config/trading') + +describe('Bug #78: Safe Orphan Removal', () => { + let manager: PositionManager + let mockDriftService: any + let mockCancelAllOrders: jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + + // Mock Drift service + mockDriftService = { + getPosition: jest.fn() + } + + const { getDriftService } = require('../../../lib/drift/client') + getDriftService.mockReturnValue(mockDriftService) + + // Mock cancelAllOrders + const ordersModule = require('../../../lib/drift/orders') + mockCancelAllOrders = jest.fn().mockResolvedValue({ success: true, cancelledCount: 2 }) + ordersModule.cancelAllOrders = mockCancelAllOrders + + // Mock Pyth price monitor + const { getPythPriceMonitor } = require('../../../lib/pyth/price-monitor') + getPythPriceMonitor.mockReturnValue({ + start: jest.fn().mockResolvedValue(undefined), + stop: jest.fn().mockResolvedValue(undefined) + }) + + // Mock trading config + const { getMarketConfig } = require('../../../config/trading') + getMarketConfig.mockReturnValue({ + driftMarketIndex: 0, + minOrderSize: 0.1 + }) + + manager = new PositionManager() + }) + + describe('When Drift Position is Closed', () => { + it('should cancel orders when Drift confirms position closed (size = 0)', async () => { + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP', + direction: 'long' + }) + + await manager.addTrade(trade) + + // Mock Drift position as closed + mockDriftService.getPosition.mockResolvedValue({ + size: 0, + side: 'none' + }) + + await manager.removeTrade(trade.id) + + // Should have called cancelAllOrders since position is closed + expect(mockCancelAllOrders).toHaveBeenCalledWith('SOL-PERP') + expect(mockCancelAllOrders).toHaveBeenCalledTimes(1) + }) + + it('should cancel orders when Drift position size is negligible (< 0.01)', async () => { + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP' + }) + + await manager.addTrade(trade) + + // Mock Drift position with negligible size + mockDriftService.getPosition.mockResolvedValue({ + size: 0.005, + side: 'long' + }) + + await manager.removeTrade(trade.id) + + // Should cancel since size is negligible + expect(mockCancelAllOrders).toHaveBeenCalledWith('SOL-PERP') + }) + }) + + describe('When Drift Position is Still Open', () => { + it('should NOT cancel orders when Drift shows open position (size >= 0.01)', async () => { + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP' + }) + + await manager.addTrade(trade) + + // Mock Drift position as still open + mockDriftService.getPosition.mockResolvedValue({ + size: 14.47, // Real position size + side: 'long', + entryPrice: 138.45 + }) + + await manager.removeTrade(trade.id) + + // Should NOT have called cancelAllOrders + expect(mockCancelAllOrders).not.toHaveBeenCalled() + }) + + it('should remove trade from tracking even when skipping order cancellation', async () => { + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP' + }) + + await manager.addTrade(trade) + + // Verify trade is tracked + expect((manager as any).activeTrades.has(trade.id)).toBe(true) + + // Mock Drift position as open + mockDriftService.getPosition.mockResolvedValue({ + size: 10.5, + side: 'long' + }) + + await manager.removeTrade(trade.id) + + // Trade should be removed from tracking + expect((manager as any).activeTrades.has(trade.id)).toBe(false) + // But orders should not be canceled + expect(mockCancelAllOrders).not.toHaveBeenCalled() + }) + + it('should log warning when skipping cancellation for safety', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() + + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP' + }) + + await manager.addTrade(trade) + + mockDriftService.getPosition.mockResolvedValue({ + size: 12.28, + side: 'long' + }) + + await manager.removeTrade(trade.id) + + // Should have logged safety warning + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('SAFETY CHECK') + ) + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Skipping order cancellation') + ) + + consoleSpy.mockRestore() + }) + }) + + describe('Error Handling', () => { + it('should NOT cancel orders on Drift query error (err on side of caution)', async () => { + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP' + }) + + await manager.addTrade(trade) + + // Mock Drift error + mockDriftService.getPosition.mockRejectedValue(new Error('RPC error')) + + await manager.removeTrade(trade.id) + + // Should NOT cancel orders on error (safety first) + expect(mockCancelAllOrders).not.toHaveBeenCalled() + + // But should remove from tracking + expect((manager as any).activeTrades.has(trade.id)).toBe(false) + }) + + it('should log error when Drift check fails', async () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation() + + const trade = createMockTrade({ + id: 'trade1', + symbol: 'SOL-PERP' + }) + + await manager.addTrade(trade) + + mockDriftService.getPosition.mockRejectedValue(new Error('Network timeout')) + + await manager.removeTrade(trade.id) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error checking Drift position') + ) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('Removing from tracking without canceling orders') + ) + + consoleSpy.mockRestore() + consoleWarnSpy.mockRestore() + }) + }) + + describe('Multiple Positions on Same Symbol', () => { + it('should preserve orders when removing old position while new position exists', async () => { + const oldTrade = createMockTrade({ + id: 'old-trade', + symbol: 'SOL-PERP', + direction: 'long' + }) + + const newTrade = createMockTrade({ + id: 'new-trade', + symbol: 'SOL-PERP', + direction: 'long' + }) + + // Add both trades (simulating orphan detection scenario) + await manager.addTrade(oldTrade) + await manager.addTrade(newTrade) + + // Mock Drift showing active position (belongs to new trade) + mockDriftService.getPosition.mockResolvedValue({ + size: 14.47, + side: 'long', + entryPrice: 138.45 + }) + + // Remove old trade (orphan cleanup) + await manager.removeTrade(oldTrade.id) + + // Should NOT cancel orders (protects new trade) + expect(mockCancelAllOrders).not.toHaveBeenCalled() + + // Old trade removed + expect((manager as any).activeTrades.has(oldTrade.id)).toBe(false) + + // New trade still tracked + expect((manager as any).activeTrades.has(newTrade.id)).toBe(true) + }) + }) +})