133 lines
5.0 KiB
TypeScript
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);
|
|
});
|