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; // Stub catalyst cache — no live Yahoo news fetches in tests (fast + offline) const stubCatalystCache = { get: async () => ({ tickers: [] as string[], stories: [] }), } 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, catalystCache: stubCatalystCache }); 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, catalystCache: stubCatalystCache }); 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, catalystCache: stubCatalystCache }); 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(); });