import test from 'node:test'; import assert from 'node:assert/strict'; import { ScreenerController } from '../server/domains/screener/screener.controller.js'; import { ScreenerEngine } from '../server/domains/screener/ScreenerEngine.js'; import type { LiveAssetResult, MarketContext, Stock, } from '../server/domains/shared/types/index.js'; import { ASSET_TYPE, SIGNAL } from '../server/domains/shared/config/constants.js'; // Mock implementations class MockScreenerEngine extends ScreenerEngine { async screenTickers(tickers: string[]) { const mockContext: MarketContext = { sp500Price: 5500, riskFreeRate: 4.5, vixLevel: 12.5, rateRegime: 'NORMAL', volatilityRegime: 'LOW', benchmarks: { marketPE: 20, techPE: 28, reitYield: 3.5, igSpread: 1.2, }, }; // Return mock results for tested tickers const mockStock = { ticker: 'AAPL', type: ASSET_TYPE.STOCK, currentPrice: 189.5, metrics: { peRatio: 28.5, returnOnEquity: 95.2, freeCashFlow: 100000000, debtToEquity: 0.75, }, getDisplayMetrics: () => ({ peRatio: 28.5, returnOnEquity: 95.2, freeCashFlow: 100000000, }), } as unknown as Stock; const mockResult: LiveAssetResult = { asset: mockStock, fundamentalScore: { label: '✓ BUY', scoreSummary: 'Quality gate PASS' }, inflatedScore: { label: '✓ BUY', scoreSummary: 'Market adjusted gate PASS' }, signal: SIGNAL.STRONG_BUY, }; return { STOCK: tickers.length > 0 ? [mockResult] : [], ETF: [], BOND: [], ERROR: [], marketContext: mockContext, }; } } class MockCatalystCache { async get() { return { tickers: ['AAPL', 'MSFT', 'NVDA'], stories: [ { headline: 'Apple beats Q2 earnings', summary: 'Strong iPhone sales boost revenue', sentiment: 'positive', relatedTickers: ['AAPL'], }, ], }; } } test('ScreenerController', async (t) => { await t.test('screen() processes valid ticker list', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const controller = new ScreenerController(engine, cache); // Create a mock FastifyInstance with only the methods we need const mockApp = { post: () => {}, get: () => {}, }; // Register should succeed controller.register(mockApp as any); assert.ok(true); }); await t.test('screen() serializes assets correctly', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const _controller = new ScreenerController(engine, cache); // Test private serialization method indirectly through results const results = await engine.screenTickers(['AAPL']); assert.equal(results.STOCK.length, 1); assert.ok(results.STOCK[0].asset.ticker === 'AAPL'); }); await t.test('screen() handles empty ticker list', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const _controller = new ScreenerController(engine, cache); const results = await engine.screenTickers([]); assert.equal(results.STOCK.length, 0); assert.equal(results.ETF.length, 0); assert.equal(results.BOND.length, 0); }); await t.test('catalysts() returns cached results', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const _controller = new ScreenerController(engine, cache); const catalysts = await cache.get(); assert.ok(Array.isArray(catalysts.tickers)); assert.ok(Array.isArray(catalysts.stories)); assert.equal(catalysts.tickers.length, 3); }); await t.test('catalysts() includes headlines and sentiment', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const _controller = new ScreenerController(engine, cache); const catalysts = await cache.get(); const story = catalysts.stories[0]; assert.ok(story.headline); assert.ok(story.sentiment); assert.ok(story.relatedTickers); }); await t.test('controller registers POST /api/screen endpoint', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const controller = new ScreenerController(engine, cache); let screenEndpointRegistered = false; const mockApp = { post: (path: string) => { if (path === '/api/screen') screenEndpointRegistered = true; }, get: () => {}, }; controller.register(mockApp as any); assert.ok(screenEndpointRegistered); }); await t.test('controller registers GET /api/screen/catalysts endpoint', async () => { const engine = new MockScreenerEngine(null as any, null as any); const cache = new MockCatalystCache() as any; const controller = new ScreenerController(engine, cache); let catalystEndpointRegistered = false; const mockApp = { post: () => {}, get: (path: string) => { if (path === '/api/screen/catalysts') catalystEndpointRegistered = true; }, }; controller.register(mockApp as any); assert.ok(catalystEndpointRegistered); }); await t.test('screen() includes market context in response', async () => { const engine = new MockScreenerEngine(null as any, null as any); const results = await engine.screenTickers(['AAPL']); assert.ok(results.marketContext); assert.ok(results.marketContext.sp500Price); assert.ok(results.marketContext.benchmarks); }); await t.test('screen() includes verdict signal in results', async () => { const engine = new MockScreenerEngine(null as any, null as any); const results = await engine.screenTickers(['AAPL']); assert.equal(results.STOCK.length, 1); const result = results.STOCK[0]; assert.ok(result.signal); assert.ok(result.fundamentalScore); assert.ok(result.inflatedScore); }); });