/ tests / utils / adaptive-concurrency.test.js
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  });