// Tests — AlertEngine (Slice 17). Pure, no network. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { AlertEngine, DedupStore, createAlert, alertTitle, alertDescription, defaultSeverity, buildDedupKey, ALERT_THROTTLE, type Alert, type AlertType, } from '../AlertEngine.ts'; // ─── Pure utility tests ────────────────────────────────────────────────────── test('defaultSeverity returns expected severity for each alert type', () => { assert.equal(defaultSeverity('drawdown_halt'), 'critical'); assert.equal(defaultSeverity('thesis_broken'), 'warning'); assert.equal(defaultSeverity('cluster_breach'), 'warning'); assert.equal(defaultSeverity('asymmetry_warning'), 'warning'); assert.equal(defaultSeverity('informed_buy'), 'info'); assert.equal(defaultSeverity('informed_sell'), 'info'); assert.equal(defaultSeverity('new_13da'), 'info'); assert.equal(defaultSeverity('rotation_incipient'), 'info'); assert.equal(defaultSeverity('regime_shift'), 'info'); assert.equal(defaultSeverity('conviction_unlock'), 'info'); assert.equal(defaultSeverity('thesis_weakening'), 'info'); }); test('alertTitle returns expected title for each alert type', () => { assert.equal(alertTitle('informed_buy'), 'Insider bought'); assert.equal(alertTitle('informed_sell', 'NVDA'), 'Insider sold for NVDA'); assert.equal(alertTitle('regime_shift'), 'Market regime changed'); assert.equal(alertTitle('drawdown_halt'), 'Drawdown tolerance breached'); assert.equal(alertTitle('informed_buy', 'AAPL'), 'Insider bought for AAPL'); }); test('alertDescription never contains trade verbs', () => { const types: AlertType[] = [ 'informed_buy', 'informed_sell', 'new_13da', 'rotation_incipient', 'regime_shift', 'conviction_unlock', 'thesis_broken', 'thesis_weakening', 'cluster_breach', 'drawdown_halt', 'asymmetry_warning', ]; const forbidden = ['buy ', 'sell ', 'cut ', 'trim ', 'buy.', 'sell.', 'cut.', 'trim.']; for (const type of types) { const desc = alertDescription(type); const lower = desc.toLowerCase(); for (const word of forbidden) { assert.ok(!lower.includes(word), `"${type}" description should not contain "${word}": "${desc.substring(0, 60)}..."`); } } }); test('alertDescription includes ADR-0007 compliant language', () => { const desc = alertDescription('drawdown_halt'); assert.ok(desc.includes('circuit breaker'), 'Should mention circuit breaker'); assert.ok(desc.includes('Existing positions continue unaffected'), 'Should mention existing positions unaffected'); assert.ok(!desc.includes('cut'), 'Should not contain "cut"'); }); test('buildDedupKey produces consistent keys', () => { assert.equal(buildDedupKey('informed_buy'), 'informed_buy'); assert.equal(buildDedupKey('informed_buy', 'NVDA'), 'informed_buy:NVDA'); assert.equal(buildDedupKey('informed_buy', 'NVDA', 'filing-123'), 'informed_buy:NVDA:filing-123'); assert.equal(buildDedupKey('drawdown_halt'), 'drawdown_halt'); }); // ─── Alert factory tests ───────────────────────────────────────────────────── test('createAlert produces a complete Alert object', () => { const alert = createAlert('id-1', 'user-1', 'informed_buy', 'NVDA'); assert.equal(alert.id, 'id-1'); assert.equal(alert.userId, 'user-1'); assert.equal(alert.type, 'informed_buy'); assert.equal(alert.symbol, 'NVDA'); assert.equal(alert.severity, 'info'); assert.ok(alert.title.startsWith('Insider bought')); assert.ok(alert.description.length > 20); assert.equal(alert.acknowledged, false); assert.ok(alert.dedupKey.startsWith('informed_buy:NVDA')); assert.ok(alert.createdAt.length > 0); assert.ok(alert.payload); }); test('createAlert includes eventId in dedupKey and payload', () => { const alert = createAlert('id-2', 'user-1', 'informed_sell', 'AAPL', 'details', 'evt-456'); assert.ok(alert.dedupKey.endsWith(':evt-456')); assert.equal(alert.payload.eventId, 'evt-456'); }); test('createAlert with details appends to description', () => { const alert = createAlert('id-3', 'user-1', 'new_13da', 'TSLA', 'Additional context about the filing.'); assert.ok(alert.description.includes('Additional context about the filing.')); }); // ─── DedupStore tests ──────────────────────────────────────────────────────── test('DedupStore tracks seen keys', () => { const store = new DedupStore(); assert.ok(!store.isDuplicate('test-key')); store.mark('test-key'); assert.ok(store.isDuplicate('test-key')); }); test('DedupStore can be reset', () => { const store = new DedupStore(); store.mark('key-1'); assert.ok(store.isDuplicate('key-1')); store.reset(); assert.ok(!store.isDuplicate('key-1')); }); // ─── AlertEngine tests ─────────────────────────────────────────────────────── test('AlertEngine.fireAndForget creates alert on first call, dedups on second', async () => { const persisted: Alert[] = []; const engine = new AlertEngine({ dedup: new DedupStore(), poll: async () => [], persist: async (a) => { persisted.push(a); return a; }, listAlerts: async () => [], acknowledge: async () => true, }); const first = await engine.fireAndForget('informed_buy', 'user-1', 'NVDA', undefined, 'filing-1'); assert.ok(first !== null, 'First call should create alert'); assert.equal(persisted.length, 1); const second = await engine.fireAndForget('informed_buy', 'user-1', 'NVDA', undefined, 'filing-1'); assert.equal(second, null, 'Duplicate should return null'); assert.equal(persisted.length, 1, 'Should not persist duplicate'); }); test('AlertEngine.fireAndForget different eventIds are not duplicates', async () => { const persisted: Alert[] = []; const engine = new AlertEngine({ dedup: new DedupStore(), poll: async () => [], persist: async (a) => { persisted.push(a); return a; }, listAlerts: async () => [], acknowledge: async () => true, }); const first = await engine.fireAndForget('informed_buy', 'user-1', 'NVDA', undefined, 'filing-1'); assert.ok(first !== null); const second = await engine.fireAndForget('informed_buy', 'user-1', 'NVDA', undefined, 'filing-2'); assert.ok(second !== null, 'Different eventId should not be duplicate'); assert.equal(persisted.length, 2); }); test('AlertEngine.pollCycle dedupes within same cycle', async () => { // If the poll function returns two alerts with the same dedup key, // only the first should be persisted. const persisted: Alert[] = []; const engine = new AlertEngine({ dedup: new DedupStore(), poll: async () => { const a = createAlert('dup-a', 'user-1', 'asymmetry_warning', 'NVDA'); const b = createAlert('dup-b', 'user-1', 'asymmetry_warning', 'NVDA'); return [a, b]; }, persist: async (a) => { persisted.push(a); return a; }, listAlerts: async () => [], acknowledge: async () => true, }); const created = await engine.pollCycle(); assert.equal(created.length, 1, 'Only one of the duplicates should be created'); assert.equal(persisted.length, 1, 'Only one should be persisted'); }); test('AlertEngine.pollCycle skips already-known dedup keys', async () => { // If a dedup key was already marked (e.g. from a prior event-driven fire), // the poll cycle should skip it. const persisted: Alert[] = []; const dedupStore = new DedupStore(); dedupStore.mark('asymmetry_warning:NVDA'); // already seen const engine = new AlertEngine({ dedup: dedupStore, poll: async () => [ createAlert('poll-1', 'user-1', 'asymmetry_warning', 'NVDA'), ], persist: async (a) => { persisted.push(a); return a; }, listAlerts: async () => [], acknowledge: async () => true, }); const created = await engine.pollCycle(); assert.equal(created.length, 0, 'Known dedup keys should be skipped'); assert.equal(persisted.length, 0, 'Nothing should be persisted'); }); test('AlertEngine start/stop polling', () => { const engine = new AlertEngine({ dedup: new DedupStore(), poll: async () => [], persist: async (a) => a, listAlerts: async () => [], acknowledge: async () => true, }, 1000); engine.start(); // Should be idempotent engine.start(); engine.stop(); // Should be safe to stop twice engine.stop(); }); // ─── Throttle tests ────────────────────────────────────────────────────────── test('ALERT_THROTTLE defines limits for all alert types', () => { const types: AlertType[] = [ 'informed_buy', 'informed_sell', 'new_13da', 'rotation_incipient', 'regime_shift', 'conviction_unlock', 'thesis_broken', 'thesis_weakening', 'cluster_breach', 'drawdown_halt', 'asymmetry_warning', ]; for (const type of types) { assert.ok(ALERT_THROTTLE[type], `Throttle config exists for ${type}`); assert.ok(ALERT_THROTTLE[type].maxPerHour > 0, `maxPerHour > 0 for ${type}`); } }); test('Critical alerts have stricter throttle', () => { assert.equal(ALERT_THROTTLE['drawdown_halt'].maxPerHour, 1); assert.equal(ALERT_THROTTLE['regime_shift'].maxPerHour, 1); assert.equal(ALERT_THROTTLE['conviction_unlock'].maxPerHour, 1); });