Files
Neon-Desk/lib/server/task-notifications.test.ts

124 lines
3.9 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import type { Task } from '@/lib/types';
import { buildTaskNotification } from '@/lib/server/task-notifications';
function baseTask(overrides: Partial<Omit<Task, 'notification'>> = {}): Omit<Task, 'notification'> {
return {
id: 'task-1',
user_id: 'user-1',
task_type: 'sync_filings',
status: 'running',
stage: 'sync.extract_taxonomy',
stage_detail: 'Extracting XBRL taxonomy for 0000320193-26-000001',
stage_context: {
progress: {
current: 2,
total: 5,
unit: 'filings'
},
counters: {
hydrated: 1,
failed: 0
},
subject: {
ticker: 'AAPL',
accessionNumber: '0000320193-26-000001'
}
},
resource_key: 'sync_filings:AAPL',
notification_read_at: null,
notification_silenced_at: null,
priority: 50,
payload: {
ticker: 'AAPL',
limit: 20
},
result: null,
error: null,
attempts: 1,
max_attempts: 3,
workflow_run_id: 'run-1',
created_at: '2026-03-09T10:00:00.000Z',
updated_at: '2026-03-09T10:05:00.000Z',
finished_at: null,
...overrides
};
}
describe('task notification builder', () => {
it('builds progress-driven notifications for running sync jobs', () => {
const notification = buildTaskNotification(baseTask());
expect(notification.title).toBe('Filing sync');
expect(notification.statusLine).toContain('Running');
expect(notification.progress?.percent).toBe(40);
expect(notification.stats.some((stat) => stat.label === 'Hydrated' && stat.value === '1')).toBe(true);
expect(notification.actions[0]).toMatchObject({
id: 'open_filings',
primary: true,
href: '/filings?ticker=AAPL'
});
});
it('builds report actions for completed analyze jobs', () => {
const notification = buildTaskNotification(baseTask({
task_type: 'analyze_filing',
status: 'completed',
stage: 'completed',
stage_detail: 'Analysis report generated for AAPL 10-Q 0000320193-26-000001.',
stage_context: {
subject: {
ticker: 'AAPL',
accessionNumber: '0000320193-26-000001',
label: '10-Q'
}
},
payload: {
accessionNumber: '0000320193-26-000001'
},
result: {
ticker: 'AAPL',
accessionNumber: '0000320193-26-000001',
filingType: '10-Q',
model: 'test-model'
},
finished_at: '2026-03-09T10:06:00.000Z'
}));
expect(notification.tone).toBe('success');
expect(notification.actions[0]).toMatchObject({
id: 'open_analysis_report',
label: 'Open summary',
primary: true
});
expect(notification.actions[0]?.href).toContain('/analysis/reports/AAPL/0000320193-26-000001');
expect(notification.stats.some((stat) => stat.label === 'Form' && stat.value === '10-Q')).toBe(true);
});
it('keeps filings navigation available for failed analyze jobs', () => {
const notification = buildTaskNotification(baseTask({
task_type: 'analyze_filing',
status: 'failed',
stage: 'analyze.fetch_document',
stage_detail: 'Could not load the primary filing document.',
error: 'Could not load the primary filing document for AAPL · 0000320193-26-000001. Retry the job after confirming the SEC source is reachable.',
stage_context: {
subject: {
ticker: 'AAPL',
accessionNumber: '0000320193-26-000001'
}
},
payload: {
accessionNumber: '0000320193-26-000001'
},
result: null,
finished_at: '2026-03-09T10:06:00.000Z'
}));
expect(notification.tone).toBe('error');
expect(notification.statusLine).toBe('Failed during fetch primary document');
expect(notification.detailLine).toBe('Could not load the primary filing document.');
expect(notification.actions.some((action) => action.id === 'open_filings')).toBe(true);
});
});