192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DigestService } from '../server/domains/digest/DigestService.js';
|
|
import { DiscordNotifier } from '../server/domains/digest/DiscordNotifier.js';
|
|
import type { SignalSnapshotRepository } from '../server/domains/shared/persistence/SignalSnapshotRepository.js';
|
|
import type { NewsRepository } from '../server/domains/news/NewsRepository.js';
|
|
import type { NewsArticleRow, SignalSnapshotRow } from '../server/domains/shared/types/index.js';
|
|
|
|
function snap(over: Partial<SignalSnapshotRow>): SignalSnapshotRow {
|
|
return {
|
|
ticker: 'AAPL',
|
|
snapshot_date: '2026-06-09',
|
|
asset_type: 'STOCK',
|
|
price: 189.5,
|
|
signal: '✅ Strong Buy',
|
|
fundamental_tier: 'PASS',
|
|
fundamental_score: 9,
|
|
fundamental_label: '🟢 BUY (High Conviction)',
|
|
inflated_tier: 'PASS',
|
|
inflated_score: 9,
|
|
inflated_label: '🟢 BUY (High Conviction)',
|
|
coverage_active: 8,
|
|
coverage_total: 11,
|
|
risk_flags: null,
|
|
rate_regime: 'NORMAL',
|
|
created_at: '2026-06-09T21:00:00.000Z',
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function article(over: Partial<NewsArticleRow>): NewsArticleRow {
|
|
return {
|
|
url_hash: 'h1',
|
|
title_hash: 't1',
|
|
ticker_list: '["AAPL"]',
|
|
headline: '8-K filing: APPLE INC',
|
|
body: null,
|
|
source: 'edgar',
|
|
catalyst: 'regulatory',
|
|
url: 'https://sec.gov/x',
|
|
published_at: '2026-06-08T20:00:00.000Z',
|
|
created_at: '2026-06-08T20:01:00.000Z',
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function makeService(
|
|
today: SignalSnapshotRow[],
|
|
prev: SignalSnapshotRow[],
|
|
newsByTicker: Record<string, NewsArticleRow[]> = {},
|
|
): DigestService {
|
|
const snapshots = {
|
|
byDate: () => today,
|
|
latestBefore: () => prev,
|
|
} as unknown as SignalSnapshotRepository;
|
|
const news = {
|
|
newsForTicker: (t: string) => newsByTicker[t] ?? [],
|
|
} as unknown as NewsRepository;
|
|
return new DigestService(snapshots, news);
|
|
}
|
|
|
|
test('DigestService', async (t) => {
|
|
await t.test('detects signal change and attaches catalysts', () => {
|
|
const service = makeService(
|
|
[snap({ signal: '🔄 Neutral', fundamental_score: 2 })],
|
|
[snap({ snapshot_date: '2026-06-08', signal: '✅ Strong Buy', fundamental_score: 9 })],
|
|
{ AAPL: [article({})] },
|
|
);
|
|
const report = service.build('2026-06-09');
|
|
assert.equal(report.changes.length, 1);
|
|
const c = report.changes[0];
|
|
assert.equal(c.previousSignal, '✅ Strong Buy');
|
|
assert.equal(c.newSignal, '🔄 Neutral');
|
|
assert.equal(c.scoreDelta, -7);
|
|
assert.equal(c.catalysts.length, 1);
|
|
assert.equal(c.catalysts[0].catalyst, 'regulatory');
|
|
});
|
|
|
|
await t.test('no change → empty digest', () => {
|
|
const service = makeService([snap({})], [snap({ snapshot_date: '2026-06-08' })]);
|
|
const report = service.build('2026-06-09');
|
|
assert.equal(report.changes.length, 0);
|
|
assert.equal(report.snapshotCount, 1);
|
|
});
|
|
|
|
await t.test('first-ever snapshot lands in newTickers, not changes', () => {
|
|
const service = makeService([snap({ ticker: 'NVDA' })], []);
|
|
const report = service.build('2026-06-09');
|
|
assert.equal(report.changes.length, 0);
|
|
assert.deepEqual(report.newTickers, ['NVDA']);
|
|
});
|
|
|
|
await t.test('M&A stories surface even without a signal change', () => {
|
|
const service = makeService(
|
|
[snap({})],
|
|
[snap({ snapshot_date: '2026-06-08' })], // same signal — no change
|
|
{
|
|
AAPL: [
|
|
article({
|
|
catalyst: 'ma',
|
|
headline: 'SC 13D filing: APPLE INC',
|
|
url_hash: 'h2',
|
|
url: 'https://sec.gov/13d',
|
|
}),
|
|
],
|
|
},
|
|
);
|
|
const report = service.build('2026-06-09');
|
|
assert.equal(report.changes.length, 0);
|
|
assert.equal(report.maStories.length, 1);
|
|
assert.ok(report.maStories[0].headline.includes('SC 13D'));
|
|
});
|
|
|
|
await t.test('sorts changes by signal-distance impact', () => {
|
|
const service = makeService(
|
|
[
|
|
snap({ ticker: 'SMALL', signal: '⚡ Momentum' }), // Strong Buy(0) → Momentum(1): impact 1
|
|
snap({ ticker: 'BIG', signal: '❌ Avoid' }), // Strong Buy(0) → Avoid(4): impact 4
|
|
],
|
|
[
|
|
snap({ ticker: 'SMALL', snapshot_date: '2026-06-08', signal: '✅ Strong Buy' }),
|
|
snap({ ticker: 'BIG', snapshot_date: '2026-06-08', signal: '✅ Strong Buy' }),
|
|
],
|
|
);
|
|
const report = service.build('2026-06-09');
|
|
assert.equal(report.changes[0].ticker, 'BIG');
|
|
assert.equal(report.changes[1].ticker, 'SMALL');
|
|
});
|
|
});
|
|
|
|
test('DiscordNotifier.buildPayload', async (t) => {
|
|
await t.test('returns null when nothing to report', () => {
|
|
assert.equal(
|
|
DiscordNotifier.buildPayload({
|
|
date: '2026-06-09',
|
|
changes: [],
|
|
newTickers: [],
|
|
maStories: [],
|
|
snapshotCount: 5,
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
await t.test('builds embed with change fields and M&A section', () => {
|
|
const payload = DiscordNotifier.buildPayload({
|
|
date: '2026-06-09',
|
|
changes: [
|
|
{
|
|
ticker: 'AAPL',
|
|
previousSignal: '✅ Strong Buy',
|
|
newSignal: '🔄 Neutral',
|
|
previousDate: '2026-06-08',
|
|
scoreDelta: -7,
|
|
price: 189.5,
|
|
catalysts: [
|
|
{
|
|
headline: '8-K filing: APPLE INC',
|
|
catalyst: 'regulatory',
|
|
source: 'edgar',
|
|
url: 'https://sec.gov/x',
|
|
publishedAt: '2026-06-08T20:00:00.000Z',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
newTickers: [],
|
|
maStories: [
|
|
{
|
|
headline: 'SC 13D filing: APPLE INC',
|
|
catalyst: 'ma',
|
|
source: 'edgar',
|
|
url: 'https://sec.gov/13d',
|
|
publishedAt: '2026-06-08T21:00:00.000Z',
|
|
},
|
|
],
|
|
snapshotCount: 12,
|
|
});
|
|
assert.ok(payload);
|
|
const embed = payload.embeds[0] as {
|
|
title: string;
|
|
fields: Array<{ name: string; value: string }>;
|
|
};
|
|
assert.ok(embed.title.includes('2026-06-09'));
|
|
assert.equal(embed.fields.length, 2); // 1 change + 1 M&A section
|
|
assert.ok(embed.fields[0].name.includes('AAPL'));
|
|
assert.ok(embed.fields[0].name.includes('score -7'));
|
|
assert.ok(embed.fields[0].value.includes('regulatory'));
|
|
assert.ok(embed.fields[1].name.includes('M&A'));
|
|
});
|
|
});
|