197 lines
6.1 KiB
TypeScript
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);
|
|
});
|
|
});
|