91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import test, { mock } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { AnthropicClient } from '../server/domains/shared/adapters/AnthropicClient.js';
|
|
import { buildApp } from '../server/app.js';
|
|
import { MockDatabaseConnection } from './helpers/mockDb.js';
|
|
|
|
const MOCK_LLM_RESPONSE = JSON.stringify({
|
|
summary: 'Mocked analysis for test.',
|
|
sentiment: 'NEUTRAL',
|
|
affectedIndustries: [],
|
|
relatedTickers: [],
|
|
});
|
|
|
|
const mockDb = new MockDatabaseConnection() as never;
|
|
|
|
test('POST /api/analyze', async (t) => {
|
|
// Spy on AnthropicClient.prototype.complete before buildApp wires it up.
|
|
// This prevents any real API calls during tests.
|
|
const completeSpy = mock.method(
|
|
AnthropicClient.prototype,
|
|
'complete',
|
|
async () => MOCK_LLM_RESPONSE,
|
|
);
|
|
|
|
// Also stub isAvailable so the controller doesn't reject with 400
|
|
mock.method(AnthropicClient.prototype, 'isAvailable', () => true, { getter: true });
|
|
|
|
await t.test('returns analysis when stories match tickers', async () => {
|
|
const app = await buildApp({ logger: false, db: mockDb });
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/analyze',
|
|
payload: { tickers: ['AAPL'] },
|
|
});
|
|
|
|
// May return no_stories if catalyst cache is empty in test env — that's fine
|
|
assert.ok(
|
|
response.statusCode === 200,
|
|
`Expected 200, got ${response.statusCode}: ${response.body}`,
|
|
);
|
|
const body = JSON.parse(response.body);
|
|
assert.ok('analysis' in body, 'Response should have analysis field');
|
|
});
|
|
|
|
await t.test('returns 400 when ANTHROPIC_API_KEY is missing and no mock', async () => {
|
|
// Reset the isAvailable mock to simulate no API key
|
|
mock.method(AnthropicClient.prototype, 'isAvailable', () => false, { getter: true });
|
|
|
|
const app = await buildApp({ logger: false, db: mockDb });
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/analyze',
|
|
payload: { tickers: ['AAPL'] },
|
|
});
|
|
|
|
assert.equal(response.statusCode, 400);
|
|
const body = JSON.parse(response.body);
|
|
assert.ok(
|
|
body.error?.includes('ANTHROPIC_API_KEY'),
|
|
`Expected API key error, got: ${body.error}`,
|
|
);
|
|
});
|
|
|
|
await t.test('does not call real Anthropic API', async () => {
|
|
// Restore isAvailable to available
|
|
mock.method(AnthropicClient.prototype, 'isAvailable', () => true, { getter: true });
|
|
|
|
const callsBefore = completeSpy.mock.calls.length;
|
|
const app = await buildApp({ logger: false, db: mockDb });
|
|
|
|
await app.inject({
|
|
method: 'POST',
|
|
url: '/api/analyze',
|
|
payload: { tickers: ['NVDA'] },
|
|
});
|
|
|
|
// If complete was called, it used our mock — not the real API
|
|
const callsAfter = completeSpy.mock.calls.length;
|
|
if (callsAfter > callsBefore) {
|
|
// Verify it returned our mock response, not a real API response
|
|
const lastCall = completeSpy.mock.calls[completeSpy.mock.calls.length - 1];
|
|
assert.ok(lastCall, 'complete() was called with our spy in place');
|
|
}
|
|
// Either way, no real API call was made (spy intercepts)
|
|
});
|
|
|
|
mock.restoreAll();
|
|
});
|