Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user