mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(drift): cap the scanner interval at the documented max
intervalMs() enforced the 15-minute floor but not the 1440-minute cap the settings route and UI apply, so a corrupted or bypass-written DB value could silently defer an enabled scan indefinitely. Clamp to the same bounds (fall back to the default outside them). Adds tests for the over-cap fallback, the isScanning overlap guard, and the route rejecting an over-cap interval.
This commit is contained in:
@@ -88,4 +88,33 @@ describe('DriftScanService', () => {
|
||||
await svc.tick(); // within the 60-minute interval => skipped
|
||||
expect(spy).toHaveBeenCalledTimes(localNodeCount()); // not doubled
|
||||
});
|
||||
|
||||
it('drops a tick that fires while a scan is already running', async () => {
|
||||
db().updateGlobalSetting('drift_scan_enabled', '1');
|
||||
db().updateGlobalSetting('drift_scan_interval_minutes', '60');
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>(r => { release = r; });
|
||||
const spy = vi.spyOn(DriftLedgerService.getInstance(), 'reconcileNode').mockImplementation(async () => {
|
||||
await gate; // hold the scan open so a second tick overlaps it
|
||||
return { stacks: 0, detected: 0, resolved: 0 };
|
||||
});
|
||||
const svc = DriftScanService.getInstance();
|
||||
const first = svc.tick(); // starts a scan and suspends on the gate (isScanning is now true)
|
||||
await Promise.resolve();
|
||||
await svc.tick(); // fires mid-scan => dropped by the isScanning guard
|
||||
release();
|
||||
await first;
|
||||
expect(spy).toHaveBeenCalledTimes(localNodeCount()); // only the first tick scanned
|
||||
});
|
||||
|
||||
it('falls back to the default interval for an out-of-range or malformed stored value', () => {
|
||||
const svc = DriftScanService.getInstance() as unknown as { intervalMs(): number };
|
||||
const defaultMs = 60 * 60_000;
|
||||
db().updateGlobalSetting('drift_scan_interval_minutes', '99999'); // above the 1440 cap
|
||||
expect(svc.intervalMs()).toBe(defaultMs);
|
||||
db().updateGlobalSetting('drift_scan_interval_minutes', '5'); // below the 15 floor
|
||||
expect(svc.intervalMs()).toBe(defaultMs);
|
||||
db().updateGlobalSetting('drift_scan_interval_minutes', 'banana'); // not a number
|
||||
expect(svc.intervalMs()).toBe(defaultMs);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,6 +286,15 @@ describe('drift detection scan settings', () => {
|
||||
expect(DatabaseService.getInstance().getGlobalSettings().drift_scan_interval_minutes).not.toBe('5');
|
||||
});
|
||||
|
||||
it('rejects an out-of-range interval above the 1440 minute cap (400)', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/settings')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ key: 'drift_scan_interval_minutes', value: '99999' });
|
||||
expect(res.status).toBe(400);
|
||||
expect(DatabaseService.getInstance().getGlobalSettings().drift_scan_interval_minutes).not.toBe('99999');
|
||||
});
|
||||
|
||||
it('rejects a non-admin write (403)', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/settings')
|
||||
|
||||
@@ -24,6 +24,7 @@ const BASE_TICK_MS = 60_000; // re-read settings and check whether a sc
|
||||
const INITIAL_DELAY_MS = 45_000; // let Docker and the node registry settle before the first scan
|
||||
const DEFAULT_INTERVAL_MINUTES = 60;
|
||||
const MIN_INTERVAL_MINUTES = 15;
|
||||
const MAX_INTERVAL_MINUTES = 1440; // matches the settings-route Zod cap; an over-cap or corrupted DB value falls back to the default rather than deferring scans indefinitely
|
||||
|
||||
export class DriftScanService {
|
||||
private static instance: DriftScanService;
|
||||
@@ -70,7 +71,7 @@ export class DriftScanService {
|
||||
private intervalMs(): number {
|
||||
try {
|
||||
const raw = Number(DatabaseService.getInstance().getGlobalSettings()['drift_scan_interval_minutes']);
|
||||
const minutes = Number.isFinite(raw) && raw >= MIN_INTERVAL_MINUTES ? raw : DEFAULT_INTERVAL_MINUTES;
|
||||
const minutes = Number.isFinite(raw) && raw >= MIN_INTERVAL_MINUTES && raw <= MAX_INTERVAL_MINUTES ? raw : DEFAULT_INTERVAL_MINUTES;
|
||||
return minutes * 60_000;
|
||||
} catch {
|
||||
return DEFAULT_INTERVAL_MINUTES * 60_000;
|
||||
|
||||
Reference in New Issue
Block a user