rate-limit-scheduler-supplement.test.js
1 /** 2 * Rate limit scheduler supplement tests 3 * Tests catch blocks in loadState/saveState/appendEvent via subprocesses. 4 * Subprocesses inherit NODE_V8_COVERAGE so coverage is tracked by c8. 5 * 6 * Targets: 7 * Lines 71-73: loadState for loop body (valid state file with entries) 8 * Lines 77-79: loadState catch (corrupt or unreadable state file) 9 * Lines 87-88: saveState catch (unwritable path) 10 * Line 46: appendEvent catch (unwritable events file path) 11 */ 12 13 import { test, describe } from 'node:test'; 14 import assert from 'node:assert/strict'; 15 import { execSync } from 'child_process'; 16 import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; 17 import { join, dirname } from 'node:path'; 18 import { tmpdir } from 'node:os'; 19 import { fileURLToPath } from 'node:url'; 20 21 const __dirname = dirname(fileURLToPath(import.meta.url)); 22 const projectRoot = join(__dirname, '../..'); 23 const rlsFile = join(projectRoot, 'src/utils/rate-limit-scheduler.js'); 24 25 function runScript(scriptContent, cwd) { 26 const scriptPath = join(cwd, 'run.mjs'); 27 writeFileSync(scriptPath, scriptContent); 28 try { 29 const stdout = execSync(`node ${scriptPath}`, { 30 cwd, 31 env: { ...process.env, LOGS_DIR: join(cwd, 'logs') }, 32 encoding: 'utf8', 33 timeout: 15000, 34 }); 35 return { stdout, exitCode: 0 }; 36 } catch (err) { 37 return { stdout: err.stdout || '', stderr: err.stderr || '', exitCode: err.status || 1 }; 38 } 39 } 40 41 describe('rate-limit-scheduler catch block coverage', () => { 42 test('loadState reads valid state entries (covers lines 71-73)', () => { 43 const tmpDir = mkdtempSync(join(tmpdir(), 'rls-valid-')); 44 mkdirSync(join(tmpDir, 'logs')); 45 46 // Create valid state file with resetAt/setAt fields so the for loop body runs 47 const validState = JSON.stringify({ 48 zenrows: { 49 resetAt: Date.now() + 3600000, 50 setAt: Date.now(), 51 stages: ['serps'], 52 reason: 'test', 53 }, 54 }); 55 writeFileSync(join(tmpDir, 'logs', 'rate-limits.json'), validState); 56 57 const result = runScript( 58 `import { isRateLimited } from ${JSON.stringify(rlsFile)}; 59 const r = isRateLimited('zenrows'); 60 console.log('checked:', r);`, 61 tmpDir 62 ); 63 64 assert.equal(result.exitCode, 0); 65 assert.ok(result.stdout.includes('checked:')); 66 rmSync(tmpDir, { recursive: true, force: true }); 67 }); 68 69 test('saveState and appendEvent catches fire when paths are directories (covers lines 46, 77-79, 87-88)', () => { 70 const tmpDir = mkdtempSync(join(tmpdir(), 'rls-dirs-')); 71 mkdirSync(join(tmpDir, 'logs')); 72 73 // Make STATE_FILE a directory → readFileSync (loadState) throws EISDIR → catch 77-79 74 // writeFileSync (saveState) throws EISDIR → catch 87-88 75 mkdirSync(join(tmpDir, 'logs', 'rate-limits.json')); 76 77 // Make EVENTS_FILE a directory → appendFileSync (appendEvent) throws EISDIR → catch 46 78 mkdirSync(join(tmpDir, 'logs', 'rate-limit-events.jsonl')); 79 80 const result = runScript( 81 `import { setRateLimit } from ${JSON.stringify(rlsFile)}; 82 // setRateLimit calls saveState (EISDIR → catch) and appendEvent (EISDIR → catch) 83 // loadState also fires at module init with EISDIR on readFileSync → catch 84 try { 85 setRateLimit('zenrows', { limitType: 'hourly' }); 86 } catch {} 87 console.log('done');`, 88 tmpDir 89 ); 90 91 // All errors are caught internally — process should succeed 92 assert.equal(result.exitCode, 0); 93 assert.ok(result.stdout.includes('done')); 94 rmSync(tmpDir, { recursive: true, force: true }); 95 }); 96 });