import assert from 'node:assert/strict'; import { config } from '../src/config/index.js'; import { healthTracker } from '../src/services/healthTracker.js'; import { ReconciliationWatchdogAutoResumeService, type WatchdogAutoResumeCycleMetrics } from '../src/services/reconciliationWatchdogAutoResumeService.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 setTradingControl = (mode: 'RUNNING' | 'PAUSED', lastChangedBy: string, lastChangedAt: number, reason?: string) => { healthTracker.recordTradingControl({ mode, lastChangedBy, lastChangedAt, reason }); }; const cleanMetrics = (): WatchdogAutoResumeCycleMetrics => ({ success: true, mismatchCount: 0, missingFromExchange: 0, missingInDb: 0, noGoTrades: 0, parityMismatchTrades: 0, parityQuarantinedTrades: 0, failedProfiles: 0, integrityWatchdogTriggered: false }); const testDoesNotResumeWhenNotPaused = () => { const service = new ReconciliationWatchdogAutoResumeService(); setTradingControl('RUNNING', 'test', Date.now(), 'baseline'); const decision = service.evaluateCycle(cleanMetrics()); assert.equal(decision.resumed, false); assert.equal(decision.reason, 'not_paused'); }; const testDoesNotResumeManualPause = () => { const service = new ReconciliationWatchdogAutoResumeService(); setTradingControl('PAUSED', 'admin-user', Date.now() - 60_000, 'manual'); const decision = service.evaluateCycle(cleanMetrics()); assert.equal(decision.resumed, false); assert.equal(decision.reason, 'pause_not_from_parity_watchdog'); assert.equal(healthTracker.isPaused(), true); }; const testResumesAfterCleanStreak = () => { const service = new ReconciliationWatchdogAutoResumeService(); const pauseAt = Date.now() - 120_000; setTradingControl('PAUSED', 'system:recon_parity_watchdog', pauseAt, 'watchdog'); const first = service.evaluateCycle(cleanMetrics()); assert.equal(first.resumed, false); assert.equal(first.cleanStreak, 1); const second = service.evaluateCycle(cleanMetrics()); assert.equal(second.resumed, true, 'Second clean cycle should auto-resume when threshold=2.'); const control = healthTracker.getSnapshot().tradingControl; assert.equal(control.mode, 'RUNNING'); assert.equal(control.lastChangedBy, 'system:recon_parity_auto_resume'); }; const testDirtyCycleResetsStreak = () => { const service = new ReconciliationWatchdogAutoResumeService(); const pauseAt = Date.now() - 120_000; setTradingControl('PAUSED', 'system:recon_parity_watchdog', pauseAt, 'watchdog'); const first = service.evaluateCycle(cleanMetrics()); assert.equal(first.cleanStreak, 1); const dirty = service.evaluateCycle({ ...cleanMetrics(), mismatchCount: 1 }); assert.equal(dirty.resumed, false); assert.equal(dirty.reason, 'cycle_not_clean'); assert.equal(dirty.cleanStreak, 0); const afterReset = service.evaluateCycle(cleanMetrics()); assert.equal(afterReset.resumed, false, 'Streak must restart after dirty cycle.'); assert.equal(afterReset.cleanStreak, 1); }; const testMinPauseWindowBlocksResume = () => { const service = new ReconciliationWatchdogAutoResumeService(); const pauseAt = Date.now() - 2_000; setTradingControl('PAUSED', 'system:recon_parity_watchdog', pauseAt, 'watchdog'); const decision = service.evaluateCycle(cleanMetrics()); assert.equal(decision.resumed, false); assert.equal(decision.reason, 'min_pause_window'); }; async function main() { const configSnapshot: Record = { ENABLE_RECON_WATCHDOG_AUTO_RESUME: mutableConfig.ENABLE_RECON_WATCHDOG_AUTO_RESUME, RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS: mutableConfig.RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS, RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES: mutableConfig.RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES, RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS: mutableConfig.RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS }; try { mutableConfig.ENABLE_RECON_WATCHDOG_AUTO_RESUME = true; mutableConfig.RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS = 5_000; mutableConfig.RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES = 2; mutableConfig.RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS = 0; testDoesNotResumeWhenNotPaused(); testDoesNotResumeManualPause(); testResumesAfterCleanStreak(); testDirtyCycleResetsStreak(); testMinPauseWindowBlocksResume(); console.log('[reconciliation-watchdog-auto-resume] OK: guard, streak, and min-pause behavior validated'); } finally { restoreConfig(configSnapshot); setTradingControl('RUNNING', 'test', Date.now(), 'reset'); } } main().catch((error) => { console.error('[reconciliation-watchdog-auto-resume] failed', error); process.exit(1); });