process.env.OPENROUTER_API_KEY = 'or-secret-key-do-not-log'; process.env.OPENROUTER_BASE_URL = 'https://openrouter.test/api/v1'; const mockAxiosPost = jest.fn(); jest.mock('axios', () => ({ post: (...args) => mockAxiosPost(...args), })); // No-op the rate limiter so tests don't burn 60s waiting for tokens. jest.mock('../../src/utils/rateLimiter', () => ({ createLimiter: () => ({ waitForToken: async () => true, snapshot: () => ({}) }), createCircuitBreaker: () => ({ call: async (fn) => fn(), snapshot: () => ({}) }), })); const adapter = require('../../src/services/adapters/openRouterAdapter'); beforeEach(() => { mockAxiosPost.mockReset(); adapter.__internals.usage.requestsToday = 0; }); describe('openRouterAdapter.configured', () => { test('reflects OPENROUTER_API_KEY presence', () => { expect(adapter.configured()).toBe(true); delete process.env.OPENROUTER_API_KEY; expect(adapter.configured()).toBe(false); process.env.OPENROUTER_API_KEY = 'or-secret-key-do-not-log'; }); }); describe('openRouterAdapter.analyze', () => { test('successful primary call returns content + model + latency', async () => { mockAxiosPost.mockResolvedValueOnce({ status: 200, data: { choices: [{ message: { content: '{"grade":"A","confidence":0.7}' } }] }, }); const result = await adapter.analyze('system', 'user'); expect(result.response).toContain('grade'); expect(result.modelUsed).toBe(adapter.__internals.PRIMARY_MODEL); expect(typeof result.latencyMs).toBe('number'); }); test('429 on primary triggers fallback model retry', async () => { mockAxiosPost .mockResolvedValueOnce({ status: 429, data: {} }) .mockResolvedValueOnce({ status: 200, data: { choices: [{ message: { content: '{"grade":"B"}' } }] }, }); const result = await adapter.analyze('s', 'u'); expect(result).not.toBeNull(); expect(result.modelUsed).toBe(adapter.__internals.FALLBACK_MODEL); expect(mockAxiosPost).toHaveBeenCalledTimes(2); }); test('both models failing returns null', async () => { mockAxiosPost .mockResolvedValueOnce({ status: 500, data: {} }) .mockResolvedValueOnce({ status: 500, data: {} }); const result = await adapter.analyze('s', 'u'); expect(result).toBeNull(); }); test('empty model response triggers fallback then null', async () => { mockAxiosPost .mockResolvedValueOnce({ status: 200, data: { choices: [] } }) .mockResolvedValueOnce({ status: 200, data: { choices: [] } }); const result = await adapter.analyze('s', 'u'); expect(result).toBeNull(); }); test('returns null when unconfigured', async () => { delete process.env.OPENROUTER_API_KEY; expect(await adapter.analyze('s', 'u')).toBeNull(); process.env.OPENROUTER_API_KEY = 'or-secret-key-do-not-log'; }); test('Bearer token is set on the request — never embedded in URL', async () => { mockAxiosPost.mockResolvedValueOnce({ status: 200, data: { choices: [{ message: { content: '{}' } }] }, }); await adapter.analyze('s', 'u'); const [url, body, opts] = mockAxiosPost.mock.calls[0]; expect(url).not.toContain('or-secret-key-do-not-log'); expect(JSON.stringify(body)).not.toContain('or-secret-key-do-not-log'); expect(opts.headers.Authorization).toBe('Bearer or-secret-key-do-not-log'); }); test('scrubError does not surface header data', () => { const err = new Error('boom'); err.code = 'ECONN'; err.response = { status: 500, headers: { authorization: 'Bearer or-secret-key-do-not-log' } }; const scrubbed = adapter.__internals.scrubError(err); expect(JSON.stringify(scrubbed)).not.toContain('or-secret-key-do-not-log'); expect(scrubbed.status).toBe(500); }); test('no `VYNDR` literal in prompt body or headers actually sent', async () => { mockAxiosPost.mockResolvedValueOnce({ status: 200, data: { choices: [{ message: { content: '{}' } }] }, }); await adapter.analyze('system msg', 'user prompt'); const [, body, opts] = mockAxiosPost.mock.calls[0]; const wirePayload = JSON.stringify({ body, headers: opts.headers }); // Brand-name check is uppercase only — vyndr.app as a referer domain // is acceptable (just a URL), but the uppercase brand string must not // be in any payload the provider sees. expect(wirePayload).not.toContain('VYNDR'); }); }); describe('openRouterAdapter.getUsage', () => { test('reports requests today + remaining cap', async () => { mockAxiosPost.mockResolvedValue({ status: 200, data: { choices: [{ message: { content: '{}' } }] }, }); await adapter.analyze('s', 'u'); const usage = adapter.getUsage(); expect(usage.requestsToday).toBeGreaterThanOrEqual(1); expect(usage.requestsRemaining).toBeLessThan(1000); }); });