Files
market_screener/tests/screener-controller.test.ts
T
2026-06-06 21:49:31 -04:00

197 lines
6.1 KiB
TypeScript

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);
});
});