import assert from 'node:assert/strict'; import { OrderStatusSyncService, type OrderStatusSyncEvent } from '../src/services/OrderStatusSyncService.js'; import type { Candle, IExchangeConnector } from '../src/connectors/types.js'; import { supabaseService } from '../src/services/SupabaseService.js'; import { config } from '../src/config/index.js'; class MockOrderSyncExchange implements IExchangeConnector { private orderResponses = new Map(); setOrder(orderId: string, payload: any) { this.orderResponses.set(orderId, payload); } async fetchOHLCV(_symbol: string, _timeframe: string, _limit?: number): Promise { return [{ timestamp: Date.now(), open: 100, high: 100, low: 100, close: 100, volume: 1 }]; } async placeOrder(_symbol: string, _side: 'buy' | 'sell', qty: number, _type: 'market' | 'limit', price?: number): Promise { return { id: `mock-order-${Date.now()}`, status: 'filled', filled_avg_price: price || 100, filled_qty: qty }; } async getPosition(_symbol: string): Promise { return null; } async getOrder(orderId: string): Promise { const response = this.orderResponses.get(orderId); if (response instanceof Error) { throw response; } return response ?? null; } cancelOrder = async (_orderId: string, _symbol?: string): Promise => true; getCapabilities = () => ({ fetchOpenOrders: true, fetchOrders: true, fetchClosedOrders: true, fetchMyTrades: true, fetchOHLCV: true, cancelOrder: true, createOrder: true, editOrder: false, fetchBalance: true, fetchLedger: false, fetchTicker: true, fetchTickers: true, fetchPosition: true, fetchPositions: true, shorting: true }); } async function run() { const exchange = new MockOrderSyncExchange(); const seenEvents: OrderStatusSyncEvent[] = []; const updateCalls: Array<{ orderId: string; status: string; fillQty?: number }> = []; const previousMissingGrace = config.ORDER_SYNC_MISSING_GRACE_MINUTES; const previousMissingConfirmations = config.ORDER_SYNC_MISSING_CONFIRMATION_COUNT; const originalGetStaleOrders = (supabaseService as any).getStaleOrders; const originalUpdateOrderStatus = (supabaseService as any).updateOrderStatus; const originalIsTradeLifecycleClosed = (supabaseService as any).isTradeLifecycleClosed; const originalGetVirtualOpenPosition = (supabaseService as any).getVirtualOpenPosition; const staleRows = [ { order_id: 'ord-partial', symbol: 'BTC/USD', status: 'pending_new', action: 'EXIT', trade_id: 'TRD-sync-1', user_id: 'user-1', profile_id: 'profile-1', created_at: new Date(Date.now() - 15 * 60 * 1000).toISOString() }, { order_id: 'ord-missing-old', symbol: 'ETH/USD', status: 'pending_new', action: 'ENTRY', trade_id: 'TRD-sync-2', user_id: 'user-1', profile_id: 'profile-1', created_at: new Date(Date.now() - 26 * 60 * 60 * 1000).toISOString() }, { order_id: 'ord-ghost-exit', symbol: 'BTC/USD', status: 'pending_new', action: 'EXIT', trade_id: 'TRD-sync-closed', user_id: 'user-1', profile_id: 'profile-1', created_at: new Date(Date.now() - 15 * 60 * 1000).toISOString() }, { order_id: 'ord-legacy-closed-error', symbol: 'ETH/USD', status: 'pending_new', trade_id: 'TRD-sync-closed-error', user_id: 'user-1', profile_id: 'profile-1', created_at: new Date(Date.now() - 20 * 60 * 1000).toISOString() }, { order_id: 'ord-transient-failure', symbol: 'ETH/USD', status: 'pending_new', action: 'EXIT', trade_id: 'TRD-sync-timeout', user_id: 'user-1', profile_id: 'profile-1', created_at: new Date(Date.now() - 20 * 60 * 1000).toISOString() }, { order_id: 'ord-legacy-no-trade', symbol: 'SOL/USD', side: 'SELL', status: 'pending_new', action: 'EXIT', user_id: 'user-1', profile_id: 'profile-1', created_at: new Date(Date.now() - 30 * 60 * 1000).toISOString() } ]; exchange.setOrder('ord-partial', { id: 'ord-partial', status: 'partially_filled', filled_avg_price: 101.25, filled_qty: '0.35' }); exchange.setOrder('ord-missing-old', null); exchange.setOrder('ord-legacy-closed-error', new Error('404 order not found')); exchange.setOrder('ord-transient-failure', new Error('socket timeout')); exchange.setOrder('ord-legacy-no-trade', null); (supabaseService as any).getStaleOrders = async () => staleRows; (supabaseService as any).isTradeLifecycleClosed = async (tradeId: string) => tradeId === 'TRD-sync-closed' || tradeId === 'TRD-sync-closed-error'; (supabaseService as any).getVirtualOpenPosition = async (profileId: string, symbol: string) => { if (profileId === 'profile-1' && symbol === 'SOL/USD') { return null; } return { profileId, symbol, side: 'BUY', qty: 1, entryPrice: 100, stopLoss: 90, takeProfit: 110, tradeId: 'TRD-open' }; }; (supabaseService as any).updateOrderStatus = async ( orderId: string, status: string, _filledAt?: Date, _price?: number, qty?: number ) => { updateCalls.push({ orderId, status, fillQty: qty }); }; try { (config as any).ORDER_SYNC_MISSING_GRACE_MINUTES = 0; (config as any).ORDER_SYNC_MISSING_CONFIRMATION_COUNT = 2; const service = new OrderStatusSyncService( exchange, 10_000, 'profile-1', (event) => { seenEvents.push(event); } ); await service.triggerSync(); const partialUpdate = updateCalls.find((c) => c.orderId === 'ord-partial'); assert(partialUpdate, 'Partial-fill stale order should be updated.'); assert.equal(partialUpdate.status, 'partially_filled'); assert.equal(Number(partialUpdate.fillQty), 0.35, 'Partial-fill quantity should be preserved in status update.'); const partialEvent = seenEvents.find((e) => e.orderId === 'ord-partial'); assert(partialEvent, 'Partial-fill stale order should emit lifecycle callback.'); assert.equal(partialEvent?.status, 'partially_filled'); assert.equal(Number(partialEvent?.fillQty), 0.35); assert.equal(Boolean(partialEvent?.quarantined), false); assert.equal( Boolean(updateCalls.find((c) => c.orderId === 'ord-missing-old')), false, 'Missing orders should not mutate on first detection in confirmation mode.' ); await service.triggerSync(); const missingOldUpdate = updateCalls.find((c) => c.orderId === 'ord-missing-old'); assert(missingOldUpdate, 'Old missing order should be quarantined to unknown.'); assert.equal(missingOldUpdate.status, 'unknown'); const missingOldEvent = seenEvents.find((e) => e.orderId === 'ord-missing-old'); assert(missingOldEvent, 'Old missing order should emit quarantined callback.'); assert.equal(missingOldEvent?.status, 'unknown'); assert.equal(Boolean(missingOldEvent?.quarantined), true); const ghostExitUpdate = updateCalls.find((c) => c.orderId === 'ord-ghost-exit'); assert(ghostExitUpdate, 'Closed lifecycle ghost EXIT should be auto-resolved.'); assert.equal(ghostExitUpdate.status, 'canceled'); const ghostExitEvent = seenEvents.find((e) => e.orderId === 'ord-ghost-exit'); assert(ghostExitEvent, 'Closed lifecycle ghost EXIT should emit callback.'); assert.equal(ghostExitEvent?.status, 'canceled'); assert.equal(Boolean(ghostExitEvent?.quarantined), false); const legacyClosedUpdate = updateCalls.find((c) => c.orderId === 'ord-legacy-closed-error'); assert(legacyClosedUpdate, 'Legacy closed lifecycle with exchange not-found exception should be auto-resolved.'); assert.equal(legacyClosedUpdate.status, 'canceled'); const legacyClosedEvent = seenEvents.find((e) => e.orderId === 'ord-legacy-closed-error'); assert(legacyClosedEvent, 'Legacy closed lifecycle should emit callback.'); assert.equal(legacyClosedEvent?.status, 'canceled'); assert.equal(Boolean(legacyClosedEvent?.quarantined), false); const transientFailureUpdate = updateCalls.find((c) => c.orderId === 'ord-transient-failure'); assert.equal(Boolean(transientFailureUpdate), false, 'Transient exchange failures must not mutate order status.'); const legacyNoTradeUpdate = updateCalls.find((c) => c.orderId === 'ord-legacy-no-trade'); assert(legacyNoTradeUpdate, 'Legacy EXIT-like order without trade_id should be auto-resolved when profile lifecycle is flat.'); assert.equal(legacyNoTradeUpdate.status, 'canceled'); const legacyNoTradeEvent = seenEvents.find((e) => e.orderId === 'ord-legacy-no-trade'); assert(legacyNoTradeEvent, 'Legacy EXIT-like order without trade_id should emit callback.'); assert.equal(legacyNoTradeEvent?.status, 'canceled'); assert.equal(Boolean(legacyNoTradeEvent?.quarantined), false); } finally { (config as any).ORDER_SYNC_MISSING_GRACE_MINUTES = previousMissingGrace; (config as any).ORDER_SYNC_MISSING_CONFIRMATION_COUNT = previousMissingConfirmations; (supabaseService as any).getStaleOrders = originalGetStaleOrders; (supabaseService as any).updateOrderStatus = originalUpdateOrderStatus; (supabaseService as any).isTradeLifecycleClosed = originalIsTradeLifecycleClosed; (supabaseService as any).getVirtualOpenPosition = originalGetVirtualOpenPosition; } console.log('[order-status-sync-regressions] OK: stale partial fill, ghost EXIT resolution, and quarantine paths validated'); } await run();