import assert from 'node:assert/strict'; import { config } from '../src/config/index.js'; import { healthTracker } from '../src/services/healthTracker.js'; import { supabaseService } from '../src/services/SupabaseService.js'; import { ReconciliationParityHeartbeatService } from '../src/services/reconciliationParityHeartbeatService.js'; type MutableConfig = typeof config & Record; const mutableConfig = config as MutableConfig; const restoreConfig = (snapshot: Record) => { for (const [key, value] of Object.entries(snapshot)) { mutableConfig[key] = value; } }; const resetTradingControl = () => { healthTracker.recordTradingControl({ mode: 'RUNNING', lastChangedBy: 'test', lastChangedAt: Date.now(), reason: 'reset' }); }; const createMockExecutor = () => { const reconcileCalls: any[] = []; return { executor: { fetchExchangePosition: async () => null, reconcileExitFill: async (...args: any[]) => { reconcileCalls.push(args); } } as any, reconcileCalls }; }; const withPatchedSupabase = async (run: (calls: { logOrderCalls: any[]; existingOrderIds: Set; }) => Promise) => { const logOrderCalls: any[] = []; const existingOrderIds = new Set(); const originalGetProfileCapital = supabaseService.getProfileCapital.bind(supabaseService); const originalGetVirtualOpenPosition = supabaseService.getVirtualOpenPosition.bind(supabaseService); const originalGetVirtualOpenPositionForTrade = supabaseService.getVirtualOpenPositionForTrade.bind(supabaseService); const originalHasLifecycleEntryOrder = supabaseService.hasLifecycleEntryOrder.bind(supabaseService); const originalHasLifecycleEntryOrderWithProfileSubTag = supabaseService.hasLifecycleEntryOrderWithProfileSubTag.bind(supabaseService); const originalGetExistingOrderIds = supabaseService.getExistingOrderIds.bind(supabaseService); const originalLogOrder = supabaseService.logOrder.bind(supabaseService); try { (supabaseService as any).getProfileCapital = async () => ({ allocatedCapital: 1000, isActive: true, userId: 'user-1' }); (supabaseService as any).getVirtualOpenPosition = async (_profileId: string, symbol: string) => { if (symbol === 'BTC/USDT') { return { profileId: 'profile-1', symbol, side: 'BUY', qty: 1, entryPrice: 100, stopLoss: 95, takeProfit: 120, userId: 'user-1', tradeId: 'trade-1', tradeIds: ['trade-1'] }; } return null; }; (supabaseService as any).getVirtualOpenPositionForTrade = async (_profileId: string, symbol: string, tradeId: string) => { if (symbol === 'BTC/USDT' && tradeId === 'trade-1') { return { profileId: 'profile-1', symbol, side: 'BUY', qty: 1, entryPrice: 100, stopLoss: 95, takeProfit: 120, userId: 'user-1', tradeId, tradeIds: [tradeId] }; } return null; }; (supabaseService as any).hasLifecycleEntryOrder = async () => true; (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = async () => true; (supabaseService as any).getExistingOrderIds = async (orderIds: string[]) => { const out = new Set(); for (const id of orderIds || []) { if (existingOrderIds.has(String(id))) out.add(String(id)); } return out; }; (supabaseService as any).logOrder = async (payload: any) => { logOrderCalls.push(payload); existingOrderIds.add(String(payload?.order_id || '')); }; await run({ logOrderCalls, existingOrderIds }); } finally { (supabaseService as any).getProfileCapital = originalGetProfileCapital; (supabaseService as any).getVirtualOpenPosition = originalGetVirtualOpenPosition; (supabaseService as any).getVirtualOpenPositionForTrade = originalGetVirtualOpenPositionForTrade; (supabaseService as any).hasLifecycleEntryOrder = originalHasLifecycleEntryOrder; (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = originalHasLifecycleEntryOrderWithProfileSubTag; (supabaseService as any).getExistingOrderIds = originalGetExistingOrderIds; (supabaseService as any).logOrder = originalLogOrder; } }; const testStreakAndIdempotency = async () => { const service = new ReconciliationParityHeartbeatService(); const { executor, reconcileCalls } = createMockExecutor(); await withPatchedSupabase(async ({ logOrderCalls }) => { const r1 = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); const r2 = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); const r3 = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); const r4 = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); assert.equal(r1.autoClosedTrades, 0); assert.equal(r2.autoClosedTrades, 0); assert.equal(r3.autoClosedTrades, 1, 'Third parity confirmation should auto-close once.'); assert.equal(r4.autoClosedTrades, 0, 'No duplicate close expected after idempotent synthetic row exists.'); assert.equal(r3.totalMismatchNotionalUsd, 100, 'Mismatch notional should capture the open trade exposure.'); assert.equal(logOrderCalls.length, 1, 'Synthetic close must be logged once.'); assert.equal(reconcileCalls.length, 1, 'Reconcile exit fill should run once.'); }); }; const testSubTagQuarantine = async () => { const service = new ReconciliationParityHeartbeatService(); const { executor, reconcileCalls } = createMockExecutor(); await withPatchedSupabase(async ({ logOrderCalls }) => { (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = async () => false; (supabaseService as any).hasLifecycleEntryOrder = async () => false; const result = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); assert.equal(result.autoClosedTrades, 0, 'Unattributed trades must never auto-close.'); assert.equal(result.quarantinedTrades, 1, 'Unattributed trades should be quarantined.'); assert.equal(result.totalMismatchNotionalUsd, 0, 'Unattributed quarantined trades should not contribute to watchdog mismatch notional.'); assert.equal(logOrderCalls.length, 0, 'No synthetic close rows should be logged.'); assert.equal(reconcileCalls.length, 0, 'No exit fill should be applied for quarantined trades.'); }); }; const testLegacyAttributionFallback = async () => { const service = new ReconciliationParityHeartbeatService(); const { executor, reconcileCalls } = createMockExecutor(); await withPatchedSupabase(async ({ logOrderCalls }) => { (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = async () => false; (supabaseService as any).hasLifecycleEntryOrder = async () => true; const result = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); assert.equal(result.quarantinedTrades, 0, 'Legacy fallback should keep attributable trades actionable.'); assert.equal(result.totalMismatchNotionalUsd, 100, 'Legacy-attributed trades should still count for mismatch notional.'); assert.equal(result.autoClosedTrades, 0, 'Single confirmation should not auto-close immediately.'); assert.equal(logOrderCalls.length, 0, 'No synthetic close should be logged on first confirmation.'); assert.equal(reconcileCalls.length, 0, 'No exit fill should be applied on first confirmation.'); }); }; const testWatchdogPause = async () => { const service = new ReconciliationParityHeartbeatService(); const { executor } = createMockExecutor(); resetTradingControl(); await withPatchedSupabase(async () => { (supabaseService as any).getProfileCapital = async () => ({ allocatedCapital: 1000, isActive: true, userId: 'user-1' }); (supabaseService as any).getVirtualOpenPosition = async (_profileId: string, symbol: string) => { if (symbol !== 'BTC/USDT') return null; return { profileId: 'profile-1', symbol, side: 'BUY', qty: 10, entryPrice: 100, stopLoss: 95, takeProfit: 120, userId: 'user-1', tradeId: 'trade-watchdog', tradeIds: ['trade-watchdog'] }; }; (supabaseService as any).getVirtualOpenPositionForTrade = async (_profileId: string, symbol: string, tradeId: string) => ({ profileId: 'profile-1', symbol, side: 'BUY', qty: 10, entryPrice: 100, stopLoss: 95, takeProfit: 120, userId: 'user-1', tradeId, tradeIds: [tradeId] }); const result = await service.runProfile({ profileId: 'profile-1', userId: 'user-1', executor, monitoredSymbols: ['BTC/USDT'] }); assert.equal(result.integrityWatchdogTriggered, true, 'Watchdog must trigger for large mismatch notional.'); assert.equal(result.totalMismatchNotionalUsd, 1000, 'Watchdog test should report cumulative mismatch notional.'); assert.equal(healthTracker.isPaused(), true, 'Trading should be paused by watchdog for high-risk mismatch.'); }); }; async function main() { const configSnapshot: Record = { ENABLE_RECON_POSITION_PARITY_HEARTBEAT: mutableConfig.ENABLE_RECON_POSITION_PARITY_HEARTBEAT, RECON_POSITION_PARITY_DRY_RUN: mutableConfig.RECON_POSITION_PARITY_DRY_RUN, RECON_POSITION_PARITY_CONFIRMATIONS: mutableConfig.RECON_POSITION_PARITY_CONFIRMATIONS, RECON_POSITION_PARITY_DUST_ABS_QTY: mutableConfig.RECON_POSITION_PARITY_DUST_ABS_QTY, RECON_POSITION_PARITY_MAX_NOTIONAL_PCT: mutableConfig.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT, RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION: mutableConfig.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION, RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION: mutableConfig.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION, RECON_EXIT_BACKFILL_DUST_ABS_QTY: mutableConfig.RECON_EXIT_BACKFILL_DUST_ABS_QTY, RECON_EXIT_BACKFILL_DUST_REL_PCT: mutableConfig.RECON_EXIT_BACKFILL_DUST_REL_PCT, ENABLE_RECON_INTEGRITY_WATCHDOG: mutableConfig.ENABLE_RECON_INTEGRITY_WATCHDOG, RECON_INTEGRITY_WATCHDOG_THROTTLE_MS: mutableConfig.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS }; try { mutableConfig.ENABLE_RECON_POSITION_PARITY_HEARTBEAT = true; mutableConfig.RECON_POSITION_PARITY_DRY_RUN = false; mutableConfig.RECON_POSITION_PARITY_CONFIRMATIONS = 3; mutableConfig.RECON_POSITION_PARITY_DUST_ABS_QTY = 0.0001; mutableConfig.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT = 0.5; mutableConfig.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION = true; mutableConfig.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION = true; mutableConfig.RECON_EXIT_BACKFILL_DUST_ABS_QTY = 0.0001; mutableConfig.RECON_EXIT_BACKFILL_DUST_REL_PCT = 0; mutableConfig.ENABLE_RECON_INTEGRITY_WATCHDOG = true; mutableConfig.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS = 0; await testStreakAndIdempotency(); await testSubTagQuarantine(); await testLegacyAttributionFallback(); await testWatchdogPause(); console.log('[reconciliation-parity-heartbeat] OK: streak, idempotency, quarantine, and watchdog behaviors validated'); } finally { restoreConfig(configSnapshot); resetTradingControl(); } } main().catch((error) => { console.error('[reconciliation-parity-heartbeat] failed', error); process.exit(1); });