// The leftover-flush watcher, driven with a fake clock and fake s3d stats. import test from 'node:test'; import assert from 'node:assert/strict'; import { createAutoFlush, clampMinutes } from '../core/autoflush.js'; function rig({ enabled = true, minutes = 5 } = {}) { const r = { t: 0, pending: { pendingObjects: 0, pendingSize: 0 }, flushes: 0, fail: null, prefs: { enabled, minutes }, down: false }; r.af = createAutoFlush({ settings: () => r.prefs, stats: async () => { if (r.down) throw new Error('refused'); return { ...r.pending }; }, flush: async () => { if (r.fail) throw new Error(r.fail); r.flushes++; r.pending = { pendingObjects: 0, pendingSize: 0 }; }, now: () => r.t, }); r.at = async (min) => { r.t = min * 60_000; await r.af.tick(); }; return r; } test('flushes once the pending set has been unchanged for N minutes', async () => { const r = rig({ minutes: 5 }); r.pending = { pendingObjects: 3, pendingSize: 1000 }; await r.at(0); await r.at(4.9); assert.equal(r.flushes, 0); assert.equal(r.af.status().dueAt, 5 * 60_000); await r.at(5); assert.equal(r.flushes, 1); assert.equal(r.af.status().lastFlushAt, 5 * 60_000); await r.at(20); assert.equal(r.flushes, 1, 'nothing pending, nothing to flush'); }); test('a new upload restarts the quiet period', async () => { const r = rig({ minutes: 5 }); r.pending = { pendingObjects: 1, pendingSize: 10 }; await r.at(0); await r.at(4); r.pending = { pendingObjects: 2, pendingSize: 20 }; await r.at(4.5); await r.at(9); assert.equal(r.flushes, 0); await r.at(9.5); assert.equal(r.flushes, 1); }); test('off means never, and s3d being down is not an error', async () => { const r = rig({ enabled: false }); r.pending = { pendingObjects: 5, pendingSize: 50 }; for (const m of [0, 10, 100]) await r.at(m); assert.equal(r.flushes, 0); assert.equal(r.af.status().dueAt, null); r.prefs.enabled = true; r.down = true; for (const m of [101, 200]) await r.at(m); assert.equal(r.flushes, 0); }); test('failed flushes back off, doubling up to an hour', async () => { const r = rig({ minutes: 2 }); r.pending = { pendingObjects: 1, pendingSize: 1 }; r.fail = 'indexer unreachable'; await r.at(0); await r.at(2); // fails; next try 4 min later assert.equal(r.af.status().lastError, 'indexer unreachable'); assert.equal(r.af.status().dueAt, 6 * 60_000); await r.at(6); // fails again; next try 8 min later assert.equal(r.af.status().dueAt, 14 * 60_000); for (let i = 0; i < 6; i++) await r.at(r.af.status().dueAt / 60_000); assert.equal(r.af.status().dueAt - r.t, 60 * 60_000, 'capped at an hour'); r.fail = null; await r.at(r.af.status().dueAt / 60_000); assert.equal(r.flushes, 1); assert.equal(r.af.status().lastError, null); r.pending = { pendingObjects: 1, pendingSize: 2 }; await r.at(r.t / 60_000); assert.equal(r.af.status().dueAt - r.t, 2 * 60_000, 'a success resets the backoff'); }); test('minutes are clamped to a sane range', () => { assert.equal(clampMinutes(0), 1); assert.equal(clampMinutes(99999), 1440); assert.equal(clampMinutes('abc'), 10); assert.equal(clampMinutes('7'), 7); });