import assert from 'node:assert/strict'; import type { Candle, IExchangeConnector } from '../src/connectors/types.js'; import { TradeExecutor } from '../src/services/TradeExecutor.js'; import { SignalDirection } from '../src/strategies/rules/types.js'; import { OrderStatusSyncService } from '../src/services/OrderStatusSyncService.js'; import { supabaseService } from '../src/services/SupabaseService.js'; class FlakyExchange implements IExchangeConnector { private failPlaceOrder = false; private failGetOrder = false; setPlaceOrderFailure(flag: boolean) { this.failPlaceOrder = flag; } setGetOrderFailure(flag: boolean) { this.failGetOrder = flag; } 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 { if (this.failPlaceOrder) { throw new Error('simulated-exchange-flap: placeOrder'); } return { id: `mock-${Date.now()}`, status: 'filled', filled_avg_price: price || 100, filled_qty: `${qty}` }; } async getPosition(_symbol: string): Promise { return null; } async getOrder(_orderId: string): Promise { if (this.failGetOrder) { throw new Error('simulated-exchange-flap: getOrder'); } return { status: 'filled', filled_avg_price: 100, filled_qty: '1' }; } } async function run() { const exchange = new FlakyExchange(); // Failure injection 1: entry should fail safely on exchange placeOrder flap. const executor = new TradeExecutor(exchange, undefined, 'global', 'failure-test'); exchange.setPlaceOrderFailure(true); const result = await executor.openPosition('BTC/USD', SignalDirection.BUY, 1, 'market'); assert.equal(result.success, false, 'openPosition must fail safely when exchange placeOrder throws.'); assert.equal(executor.getActivePosition('BTC/USD'), null, 'No local position should be created on failed order placement.'); // Failure injection 2: stale-order sync should absorb getOrder flap without crashing or mutating status. const originalGetStaleOrders = (supabaseService as any).getStaleOrders; const originalUpdateOrderStatus = (supabaseService as any).updateOrderStatus; const statusWrites: string[] = []; (supabaseService as any).getStaleOrders = async () => [{ order_id: 'ord-flaky', symbol: 'BTC/USD', status: 'pending_new', created_at: new Date(Date.now() - 10 * 60 * 1000).toISOString() }]; (supabaseService as any).updateOrderStatus = async (_orderId: string, status: string) => { statusWrites.push(status); }; exchange.setGetOrderFailure(true); try { const sync = new OrderStatusSyncService(exchange, 5_000); await sync.triggerSync(); assert.equal(statusWrites.length, 0, 'Order status should not be mutated when exchange getOrder fails transiently.'); } finally { (supabaseService as any).getStaleOrders = originalGetStaleOrders; (supabaseService as any).updateOrderStatus = originalUpdateOrderStatus; } console.log('[failure-injection] OK: exchange/API flap paths handled safely'); } await run();