import assert from 'node:assert/strict'; import { config } from '../src/config/index.js'; import { AIClient } from '../src/services/aiClient.js'; import { AlpacaConnector } from '../src/connectors/alpaca.js'; import { CCXTConnector } from '../src/connectors/ccxt.js'; import { ConnectorFactory } from '../src/connectors/factory.js'; import type { Candle } from '../src/connectors/types.js'; async function* generateBars(rows: any[]) { for (const row of rows) { yield row; } } async function testAIClient() { const originalAiConfig = { OPENAI_API_KEY: config.AI.OPENAI_API_KEY, PERPLEXITY_API_KEY: config.AI.PERPLEXITY_API_KEY, GEMINI_API_KEY: config.AI.GEMINI_API_KEY, FALLBACK_LIST: [...config.AI.FALLBACK_LIST], MODEL: config.AI.MODEL }; try { config.AI['OPENAI_API_KEY'] = ''; config.AI['PERPLEXITY_API_KEY'] = ''; config.AI['GEMINI_API_KEY'] = ''; config.AI.FALLBACK_LIST = ['openai', 'perplexity', 'gemini']; const clientNoKeys = new AIClient(); const noResult = await clientNoKeys.generateAnalysis('prompt'); assert.equal(noResult, null, 'generateAnalysis should return null when no provider keys are configured'); const noProbeHealth = await clientNoKeys.getProviderHealth(false); assert.equal(noProbeHealth.length, 3); assert(noProbeHealth.every((item) => item.status === 'missing_key')); // Override provider call paths to avoid network and validate fallback routing const clientWithMocks = new AIClient() as any; config.AI['OPENAI_API_KEY'] = 'key-openai'; config.AI['PERPLEXITY_API_KEY'] = 'key-perplexity'; config.AI['GEMINI_API_KEY'] = 'key-gemini'; config.AI.FALLBACK_LIST = ['unsupported' as any, 'openai', 'perplexity', 'gemini']; clientWithMocks.callOpenAI = async () => '{"action":"BUY","confidence":75,"reasoning":"ok"}'; clientWithMocks.callPerplexity = async () => '{"action":"SELL","confidence":65,"reasoning":"ok"}'; clientWithMocks.callGemini = async () => '{"action":"HOLD","confidence":50,"reasoning":"ok"}'; const mockedResult = await clientWithMocks.generateAnalysis('probe prompt'); assert.equal(typeof mockedResult, 'string'); assert((mockedResult as string).includes('BUY')); const probeHealth = await clientWithMocks.getProviderHealth(true); assert.equal(probeHealth.length, 3); assert(probeHealth.every((item: any) => item.status === 'ok')); // probeProvider unsupported branch await assert.rejects(async () => { await clientWithMocks.probeProvider('unsupported'); }); } finally { config.AI['OPENAI_API_KEY'] = originalAiConfig.OPENAI_API_KEY; config.AI['PERPLEXITY_API_KEY'] = originalAiConfig.PERPLEXITY_API_KEY; config.AI['GEMINI_API_KEY'] = originalAiConfig.GEMINI_API_KEY; config.AI.FALLBACK_LIST = originalAiConfig.FALLBACK_LIST; config.AI.MODEL = originalAiConfig.MODEL; } } async function testAlpacaConnector() { const originalAssetClass = config.ASSET_CLASS; try { const connector = new AlpacaConnector('k', 's') as any; const bars = [ { Timestamp: '2026-02-16T00:00:00Z', OpenPrice: 100, HighPrice: 101, LowPrice: 99, ClosePrice: 100, Volume: 10 }, { Timestamp: '2026-02-16T01:00:00Z', OpenPrice: 101, HighPrice: 102, LowPrice: 100, ClosePrice: 101, Volume: 11 }, { Timestamp: '2026-02-16T02:00:00Z', OpenPrice: 102, HighPrice: 103, LowPrice: 101, ClosePrice: 102, Volume: 12 }, { Timestamp: '2026-02-16T03:00:00Z', OpenPrice: 103, HighPrice: 104, LowPrice: 102, ClosePrice: 103, Volume: 13 } ]; connector.client = { getBarsV2: (_symbol: string, _opts: any) => generateBars(bars), createOrder: async (opts: any) => ({ id: 'alp-1', ...opts }), getOrder: async (_id: string) => ({ id: 'alp-1', status: 'filled' }), cancelOrder: async (_id: string) => undefined, getPosition: async (_symbol: string) => ({ side: 'long', avg_entry_price: '100', qty: '2' }), getClock: async () => ({ is_open: true }) }; const mappedTf = connector.mapTimeframe('1m'); assert.equal(mappedTf, '1Min'); assert.equal(connector.mapTimeframe('unknown'), 'unknown'); const aggregated = connector.aggregateBars([ { timestamp: 1, open: 1, high: 2, low: 0.5, close: 1.5, volume: 5 }, { timestamp: 2, open: 1.5, high: 3, low: 1, close: 2.5, volume: 7 } ], 2); assert.equal(aggregated.length, 1); assert.equal(aggregated[0].high, 3); assert.equal(aggregated[0].volume, 12); const candles4h = await connector.fetchOHLCV('BTC/USD', '4h', 1); assert.equal(candles4h.length, 1); config.ASSET_CLASS = 'us_equity'; const orderWithBracket = await connector.placeOrder('AAPL', 'buy', 1, 'limit', 200, 190, 220); assert.equal(orderWithBracket.order_class, 'bracket'); config.ASSET_CLASS = 'crypto'; const cryptoOrder = await connector.placeOrder('BTC/USD', 'sell', 0.5, 'market'); assert.equal(cryptoOrder.side, 'sell'); const order = await connector.getOrder('ord-1'); assert.equal(order.status, 'filled'); const cancelOk = await connector.cancelOrder('ord-1'); assert.equal(cancelOk, true); const position = await connector.getPosition('BTC/USD'); assert.equal(position.qty, '2'); const marketOpen = await connector.isTradingWindowOpen(); assert.equal(marketOpen, true); connector.client.getOrder = async () => { throw new Error('boom'); }; const missingOrder = await connector.getOrder('ord-2'); assert.equal(missingOrder, null); connector.client.cancelOrder = async () => { throw new Error('boom'); }; const cancelFail = await connector.cancelOrder('ord-2'); assert.equal(cancelFail, false); connector.client.getPosition = async () => { throw new Error('404 not found'); }; const noPosition = await connector.getPosition('ETH/USD'); assert.equal(noPosition, null); connector.client.getPosition = async () => { throw new Error('500 server'); }; await assert.rejects(async () => { await connector.getPosition('ETH/USD'); }); config.ASSET_CLASS = 'us_equity'; connector.client.getClock = async () => ({ is_open: false }); const marketClosed = await connector.isTradingWindowOpen(); assert.equal(marketClosed, false); connector.client.getClock = async () => ({ nope: true }); const marketUnknown = await connector.isTradingWindowOpen(); assert.equal(marketUnknown, null); connector.client.getClock = async () => { throw new Error('clock-fail'); }; const clockFail = await connector.isTradingWindowOpen(); assert.equal(clockFail, null); } finally { config.ASSET_CLASS = originalAssetClass; } } async function testCCXTConnectorAndFactory() { const originalExchange = config.EXCHANGE; const originalProvider = config.PROVIDER; try { // Unsupported exchange branch config.EXCHANGE = 'definitely_unsupported_exchange'; await assert.rejects(async () => { new CCXTConnector('k', 's'); }); config.EXCHANGE = 'binance'; const ccxtConnector = new CCXTConnector('k', 's') as any; ccxtConnector.client = { fetchOHLCV: async (_symbol: string, _tf: string, _since: any, _limit: number) => [ [1, 100, 110, 90, 105, 1000], [2, 105, 112, 100, 111, 1200] ], createOrder: async (_symbol: string, _type: string, _side: string, _qty: number, _price: number | undefined) => ({ id: 'ccxt-1' }), fetchOrder: async (_id: string, _symbol?: string) => ({ id: 'ccxt-1', status: 'closed' }), fetchPositions: async (_symbols: string[]) => [{ symbol: 'BTC/USDT', contracts: 1 }], cancelOrder: async (_id: string, _symbol?: string) => undefined }; const candles = await ccxtConnector.fetchOHLCV('BTC/USDT', '1Hour', 2); assert.equal(candles.length, 2); assert.equal((candles[0] as Candle).close, 105); const order = await ccxtConnector.placeOrder('BTC/USDT', 'buy', 1, 'market'); assert.equal(order.id, 'ccxt-1'); const fetchedOrder = await ccxtConnector.getOrder('ccxt-1', 'BTC/USDT'); assert.equal(fetchedOrder.status, 'closed'); const position = await ccxtConnector.getPosition('BTC/USDT'); assert.equal(position.symbol, 'BTC/USDT'); const isOpen = await ccxtConnector.isTradingWindowOpen(); assert.equal(isOpen, true); const cancelOk = await ccxtConnector.cancelOrder('ccxt-1', 'BTC/USDT'); assert.equal(cancelOk, true); ccxtConnector.client.fetchOrder = async () => { throw new Error('fetch-order-fail'); }; const missingOrder = await ccxtConnector.getOrder('ccxt-2', 'BTC/USDT'); assert.equal(missingOrder, null); ccxtConnector.client.fetchPositions = async () => { throw new Error('position-fail'); }; await assert.rejects(async () => { await ccxtConnector.getPosition('BTC/USDT'); }); ccxtConnector.client.cancelOrder = async () => { throw new Error('cancel-fail'); }; const cancelFail = await ccxtConnector.cancelOrder('ccxt-2', 'BTC/USDT'); assert.equal(cancelFail, false); // Factory branches config.PROVIDER = 'alpaca'; const defaultConnector = ConnectorFactory.getConnector(); assert(defaultConnector instanceof AlpacaConnector); const customCcxt = ConnectorFactory.getCustomConnector('ccxt', 'k', 's'); assert(customCcxt instanceof CCXTConnector); const customAlpaca = ConnectorFactory.getCustomConnector('alpaca', 'k', 's'); assert(customAlpaca instanceof AlpacaConnector); await assert.rejects(async () => { ConnectorFactory.getCustomConnector('unknown-provider'); }); } finally { config.EXCHANGE = originalExchange; config.PROVIDER = originalProvider; } } async function run() { await testAIClient(); await testAlpacaConnector(); await testCCXTConnectorAndFactory(); console.log('[connector-ai-coverage] OK: connector and AI client branches validated'); } await run();