learning_ai_invt_trdg/backend/testReconciliationWatchdogAutoResume.ts

133 lines
5.0 KiB
TypeScript

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<string, any>;
const mutableConfig = config as MutableConfig;
const restoreConfig = (snapshot: Record<string, unknown>) => {
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<string, unknown> = {
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);
});