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 { 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 { 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 = {}, ): 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')); }); });