adaptive-concurrency.test.js
1 /** 2 * Tests for Adaptive Concurrency Module 3 */ 4 5 import { test, describe, mock, beforeEach, afterEach } from 'node:test'; 6 import assert from 'node:assert'; 7 8 // Mock dependencies before importing 9 let mockLoadavg; 10 let mockCpus; 11 let mockFreemem; 12 let mockExecSync; 13 let mockReadFileSync; 14 let mockGetCurrentCpuUsage; 15 16 mock.module('os', { 17 namedExports: { 18 loadavg: () => mockLoadavg(), 19 cpus: () => mockCpus(), 20 freemem: () => mockFreemem(), 21 }, 22 }); 23 24 mock.module('child_process', { 25 namedExports: { 26 execSync: (...args) => mockExecSync(...args), 27 }, 28 }); 29 30 mock.module('fs', { 31 namedExports: { 32 readFileSync: (...args) => mockReadFileSync(...args), 33 appendFileSync: () => {}, 34 }, 35 }); 36 37 mock.module('../../src/utils/cpu-monitor.js', { 38 namedExports: { 39 getCurrentCpuUsage: () => mockGetCurrentCpuUsage(), 40 }, 41 }); 42 43 const { getAdaptiveConcurrency, getAdaptiveConcurrencyFast, isScreenActive } = 44 await import('../../src/utils/adaptive-concurrency.js'); 45 46 describe('Adaptive Concurrency', () => { 47 beforeEach(() => { 48 // Defaults: 4 CPUs, low load, plenty of memory, screen off 49 mockCpus = () => [1, 2, 3, 4]; // 4 CPUs (array length matters) 50 mockLoadavg = () => [0.4, 0.5, 0.6]; // 1-min, 5-min, 15-min 51 mockFreemem = () => 4 * 1024 * 1024 * 1024; // 4 GB free 52 mockExecSync = () => Buffer.from('Monitor is Off'); // screen off 53 mockReadFileSync = () => ''; // empty .env 54 mockGetCurrentCpuUsage = () => 0.1; // low CPU 55 }); 56 57 describe('isScreenActive', () => { 58 test('returns false when monitor is off', () => { 59 mockExecSync = () => Buffer.from('DPMS (Energy Star):\n Monitor is Off'); 60 // Reset cache by waiting (or just calling) 61 const result = isScreenActive(); 62 assert.strictEqual(result, false); 63 }); 64 65 test('returns false when xset fails (no display)', () => { 66 mockExecSync = () => { 67 throw new Error('unable to open display'); 68 }; 69 const result = isScreenActive(); 70 assert.strictEqual(result, false); 71 }); 72 }); 73 74 describe('getAdaptiveConcurrency', () => { 75 test('returns ceiling when load is below ease threshold', () => { 76 // Load: 0.4/4 = 0.1 (normalized), well below EASE_LOAD of 0.4 77 mockLoadavg = () => [0.4, 0.5, 0.6]; 78 const result = getAdaptiveConcurrency(1, 10); 79 assert.strictEqual(result, 10); 80 }); 81 82 test('returns minimum when load exceeds max threshold', () => { 83 // Load: 4.0/4 = 1.0 (normalized), above MAX_LOAD of 0.8 84 mockLoadavg = () => [4.0, 3.0, 2.0]; 85 const result = getAdaptiveConcurrency(1, 10); 86 assert.strictEqual(result, 1); 87 }); 88 89 test('returns interpolated value between ease and max', () => { 90 // Load: 2.4/4 = 0.6 (normalized), between EASE_LOAD=0.4 and MAX_LOAD=0.8 91 // t = (0.6 - 0.4) / (0.8 - 0.4) = 0.5 92 // result = max(1, round(10 - 0.5 * (10 - 1))) = max(1, round(10 - 4.5)) = 6 93 mockLoadavg = () => [2.4, 2.0, 1.5]; 94 const result = getAdaptiveConcurrency(1, 10); 95 assert.ok(result > 1, 'Should be above minimum'); 96 assert.ok(result < 10, 'Should be below ceiling'); 97 }); 98 99 test('returns minimum when memory is below floor', () => { 100 // Very low memory: 256 MB (below default floor of 768 MB) 101 mockFreemem = () => 256 * 1024 * 1024; 102 mockLoadavg = () => [0.1, 0.1, 0.1]; // Low load 103 const result = getAdaptiveConcurrency(2, 20); 104 assert.strictEqual(result, 2); // Should be forced to minimum 105 }); 106 107 test('reads ceiling from env var when envKey provided', () => { 108 mockLoadavg = () => [0.1, 0.1, 0.1]; // Low load → should return ceiling 109 mockReadFileSync = () => 'BROWSER_CONCURRENCY=5\n'; 110 const result = getAdaptiveConcurrency(1, 10, 'BROWSER_CONCURRENCY'); 111 assert.strictEqual(result, 5); 112 }); 113 114 test('uses defaultMax when env var not found', () => { 115 mockLoadavg = () => [0.1, 0.1, 0.1]; 116 mockReadFileSync = () => 'OTHER_VAR=99\n'; 117 const result = getAdaptiveConcurrency(1, 10, 'BROWSER_CONCURRENCY'); 118 assert.strictEqual(result, 10); 119 }); 120 121 test('uses defaultMax when .env file does not exist', () => { 122 mockLoadavg = () => [0.1, 0.1, 0.1]; 123 mockReadFileSync = () => { 124 throw new Error('ENOENT'); 125 }; 126 const result = getAdaptiveConcurrency(1, 10, 'BROWSER_CONCURRENCY'); 127 assert.strictEqual(result, 10); 128 }); 129 130 test('never returns below minimum', () => { 131 // Extreme load 132 mockLoadavg = () => [100, 100, 100]; 133 const result = getAdaptiveConcurrency(3, 10); 134 assert.ok(result >= 3, 'Should never go below minimum'); 135 }); 136 137 test('handles single CPU system', () => { 138 mockCpus = () => [1]; // 1 CPU 139 mockLoadavg = () => [0.1, 0.1, 0.1]; // Load 0.1/1 = 0.1 140 const result = getAdaptiveConcurrency(1, 5); 141 assert.strictEqual(result, 5); // Below ease threshold 142 }); 143 }); 144 145 describe('getAdaptiveConcurrencyFast', () => { 146 test('uses getCurrentCpuUsage instead of loadavg', () => { 147 let loadavgCalled = false; 148 mockLoadavg = () => { 149 loadavgCalled = true; 150 return [4.0, 4.0, 4.0]; // high load 151 }; 152 mockGetCurrentCpuUsage = () => 0.1; // low CPU (should return ceiling) 153 154 const result = getAdaptiveConcurrencyFast(1, 10); 155 assert.strictEqual(result, 10); // Based on low CPU, not high loadavg 156 }); 157 158 test('returns minimum when CPU is high', () => { 159 mockGetCurrentCpuUsage = () => 0.95; 160 const result = getAdaptiveConcurrencyFast(1, 10); 161 assert.strictEqual(result, 1); 162 }); 163 164 test('returns ceiling when CPU is low', () => { 165 mockGetCurrentCpuUsage = () => 0.05; 166 const result = getAdaptiveConcurrencyFast(1, 10); 167 assert.strictEqual(result, 10); 168 }); 169 170 test('returns minimum when memory constrained', () => { 171 mockFreemem = () => 100 * 1024 * 1024; // 100 MB 172 mockGetCurrentCpuUsage = () => 0.05; // low CPU 173 const result = getAdaptiveConcurrencyFast(2, 20); 174 assert.strictEqual(result, 2); 175 }); 176 177 test('reads ceiling from env var', () => { 178 mockGetCurrentCpuUsage = () => 0.05; 179 mockReadFileSync = () => 'ENRICHMENT_CONCURRENCY=3\n'; 180 const result = getAdaptiveConcurrencyFast(1, 10, 'ENRICHMENT_CONCURRENCY'); 181 assert.strictEqual(result, 3); 182 }); 183 }); 184 });