diff --git a/lib/automation-control.js b/lib/automation-control.js index f3e67ab..fbd4578 100644 --- a/lib/automation-control.js +++ b/lib/automation-control.js @@ -14,10 +14,20 @@ return Array.isArray(value) ? value : []; } + function normalizeQueueLimit(value) { + const isNumber = typeof value === 'number'; + const isString = typeof value === 'string'; + if (!isNumber && !isString) return 15000; + if (isString && value.trim() === '') return 15000; + const number = Number(value); + if (!Number.isFinite(number) || number < 0) return 15000; + if (number === 0 && isString && value.trim() !== '0') return 15000; + return Math.floor(number); + } + function normalizeAutomationSettings(value = {}) { const settings = asObject(value); - const rawLimit = Number(settings.queueLimitJobs); - const queueLimitJobs = Number.isFinite(rawLimit) && rawLimit >= 0 ? Math.floor(rawLimit) : 15000; + const queueLimitJobs = normalizeQueueLimit(settings.queueLimitJobs); const rawInterval = Number(settings.reconcileIntervalMinutes); return { queueLimitJobs, @@ -39,8 +49,7 @@ ); const rawCurrentJobCount = Number(value.currentJobCount); const currentJobCount = Number.isFinite(rawCurrentJobCount) ? Math.max(0, Math.floor(rawCurrentJobCount)) : 0; - const rawQueueLimitJobs = value.queueLimitJobs === undefined ? 15000 : Number(value.queueLimitJobs); - const queueLimitJobs = Number.isFinite(rawQueueLimitJobs) && rawQueueLimitJobs >= 0 ? Math.floor(rawQueueLimitJobs) : 15000; + const queueLimitJobs = normalizeQueueLimit(value.queueLimitJobs); let available = queueLimitJobs === 0 ? Number.POSITIVE_INFINITY : Math.max(0, queueLimitJobs - currentJobCount); const admittedPaths = []; const deferredPaths = []; diff --git a/tests/automation-control.test.js b/tests/automation-control.test.js index 378eb08..ecd2fd2 100644 --- a/tests/automation-control.test.js +++ b/tests/automation-control.test.js @@ -49,6 +49,16 @@ test('automation settings normalize invalid limits intervals and pause timestamp }); }); +test('automation settings allow only numeric and trimmed string zero to disable the queue limit', () => { + const invalidLimits = [null, '', ' ', false, true, {}, undefined, NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '0.0', '00', '+0', '-0']; + assert.deepEqual( + invalidLimits.map((queueLimitJobs) => normalizeAutomationSettings({ queueLimitJobs }).queueLimitJobs), + invalidLimits.map(() => 15000) + ); + assert.equal(normalizeAutomationSettings({ queueLimitJobs: 0 }).queueLimitJobs, 0); + assert.equal(normalizeAutomationSettings({ queueLimitJobs: ' 0 ' }).queueLimitJobs, 0); +}); + test('capacity counts only executable and running queue jobs', () => { const statuses = ['preview', 'queued', 'getting-server', 'uploading', 'retrying', 'done', 'error', 'aborted', 'skipped']; assert.equal(countAutomaticQueueJobs(statuses.map((status, index) => ({ id: String(index), status }))), 5); @@ -111,6 +121,31 @@ test('admission tolerates malformed candidates and numeric inputs', () => { }); }); +test('admission allows only numeric and trimmed string zero to disable the queue limit', () => { + const invalidLimits = [null, '', ' ', false, true, {}, undefined, NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '0.0', '00', '+0', '-0']; + const candidate = { path: 'large.mkv', mtimeMs: 1, eligibleJobCount: 16000 }; + assert.deepEqual( + invalidLimits.map((queueLimitJobs) => planAtomicAdmissions({ candidates: [candidate], queueLimitJobs })), + invalidLimits.map(() => ({ + admittedPaths: [], + deferredPaths: ['large.mkv'], + currentJobCount: 0, + plannedJobs: 0, + availableSlots: 15000 + })) + ); + assert.deepEqual( + [0, ' 0 '].map((queueLimitJobs) => planAtomicAdmissions({ candidates: [candidate], queueLimitJobs })), + [0, ' 0 '].map(() => ({ + admittedPaths: ['large.mkv'], + deferredPaths: [], + currentJobCount: 0, + plannedJobs: 16000, + availableSlots: null + })) + ); +}); + test('daily telemetry resets atomically on the local calendar day boundary', () => { const before = { dateKey: '2026-08-25', detected: 8, queued: 4, skipped: 2, deferred: 1 }; const now = new Date(2026, 7, 26, 0, 0, 1).getTime();