/ tests / utils / rate-limit-scheduler-supplement.test.js
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  });