/ tests / cron / monitor-openrouter-credits.test.js
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  });