monitor-openrouter-credits.test.js
1 /** 2 * Tests for src/cron/monitor-openrouter-credits.js 3 * 4 * Covers all branches: 5 * - Line 21-23: OPENROUTER_API_KEY not configured → early return 6 * - Line 29: status.remaining === null (unlimited/free tier) 7 * - Lines 32-40: status.remaining has value + burnRate + daysLeft 8 * - Lines 45-57: status.alert present vs absent 9 * - Lines 60-63: error thrown → catch block 10 */ 11 12 import { test, describe, mock, before, after } from 'node:test'; 13 import assert from 'node:assert/strict'; 14 15 // Control what monitorCredits returns 16 let mockMonitorCreditsResult = { 17 remaining: 25.0, 18 burnRate: 2.5, 19 daysLeft: 10.0, 20 alert: null, 21 }; 22 let mockMonitorCreditsThrows = false; 23 24 await mock.module('../../src/utils/openrouter-monitor.js', { 25 namedExports: { 26 monitorCredits: async () => { 27 if (mockMonitorCreditsThrows) { 28 throw new Error('Network error: connection refused'); 29 } 30 return mockMonitorCreditsResult; 31 }, 32 }, 33 }); 34 35 await mock.module('../../src/utils/load-env.js', { 36 defaultExport: {}, 37 }); 38 39 const { default: main } = await import('../../src/cron/monitor-openrouter-credits.js'); 40 41 // ─── Helpers ───────────────────────────────────────────────────────────────── 42 43 function setResult(overrides) { 44 mockMonitorCreditsResult = { 45 remaining: 25.0, 46 burnRate: 2.5, 47 daysLeft: 10.0, 48 alert: null, 49 ...overrides, 50 }; 51 mockMonitorCreditsThrows = false; 52 } 53 54 // ─── Tests ──────────────────────────────────────────────────────────────────── 55 56 describe('monitor-openrouter-credits', () => { 57 let originalApiKey; 58 59 before(() => { 60 originalApiKey = process.env.OPENROUTER_API_KEY; 61 }); 62 63 after(() => { 64 if (originalApiKey !== undefined) { 65 process.env.OPENROUTER_API_KEY = originalApiKey; 66 } else { 67 delete process.env.OPENROUTER_API_KEY; 68 } 69 }); 70 71 describe('OPENROUTER_API_KEY not configured (lines 20-23)', () => { 72 test('returns early without calling monitorCredits when key is missing', async () => { 73 delete process.env.OPENROUTER_API_KEY; 74 75 // Should not throw and should return undefined (early return) 76 const result = await main(); 77 assert.equal(result, undefined, 'should return undefined on early exit'); 78 }); 79 }); 80 81 describe('status.remaining === null — unlimited/free tier (line 29)', () => { 82 test('logs "Unlimited" message when remaining is null', async () => { 83 process.env.OPENROUTER_API_KEY = 'test-key'; 84 setResult({ remaining: null, burnRate: null, daysLeft: null }); 85 86 // Should not throw 87 await assert.doesNotReject(main(), 'should complete without error'); 88 }); 89 }); 90 91 describe('status.remaining with burnRate and daysLeft (lines 32-40)', () => { 92 test('logs balance, burn rate, and days remaining when all values present', async () => { 93 process.env.OPENROUTER_API_KEY = 'test-key'; 94 setResult({ remaining: 15.5, burnRate: 1.5, daysLeft: 10.3 }); 95 96 await assert.doesNotReject(main(), 'should complete without error'); 97 }); 98 99 test('handles burnRate=null (no burn rate available)', async () => { 100 process.env.OPENROUTER_API_KEY = 'test-key'; 101 setResult({ remaining: 30.0, burnRate: null, daysLeft: null }); 102 103 await assert.doesNotReject(main(), 'should complete without error'); 104 }); 105 106 test('handles daysLeft=null when burnRate is available', async () => { 107 process.env.OPENROUTER_API_KEY = 'test-key'; 108 setResult({ remaining: 20.0, burnRate: 3.0, daysLeft: null }); 109 110 await assert.doesNotReject(main(), 'should complete without error'); 111 }); 112 }); 113 114 describe('status.alert present (lines 45-48)', () => { 115 test('logs alert when status has an alert object', async () => { 116 process.env.OPENROUTER_API_KEY = 'test-key'; 117 setResult({ 118 remaining: 2.0, 119 burnRate: 5.0, 120 daysLeft: 0.4, 121 alert: { 122 level: 'critical', 123 message: 'Credits critically low! Only $2.00 remaining.', 124 }, 125 }); 126 127 await assert.doesNotReject(main(), 'should complete without error even with alert'); 128 }); 129 130 test('logs warning-level alert', async () => { 131 process.env.OPENROUTER_API_KEY = 'test-key'; 132 setResult({ 133 remaining: 8.0, 134 burnRate: 2.0, 135 daysLeft: 4.0, 136 alert: { 137 level: 'warning', 138 message: 'Credits running low. $8.00 remaining.', 139 }, 140 }); 141 142 await assert.doesNotReject(main(), 'should complete without error'); 143 }); 144 }); 145 146 describe('status.alert absent — credits OK (line 55-57)', () => { 147 test('logs credits OK when remaining > threshold with no alert', async () => { 148 process.env.OPENROUTER_API_KEY = 'test-key'; 149 setResult({ remaining: 50.0, burnRate: 1.0, daysLeft: 50.0, alert: null }); 150 151 await assert.doesNotReject(main(), 'should complete without error'); 152 }); 153 }); 154 155 describe('error thrown from monitorCredits (lines 60-63)', () => { 156 test('re-throws error from monitorCredits', async () => { 157 process.env.OPENROUTER_API_KEY = 'test-key'; 158 mockMonitorCreditsThrows = true; 159 160 await assert.rejects(main(), /Network error/, 'should re-throw the error'); 161 162 mockMonitorCreditsThrows = false; 163 }); 164 }); 165 });