Files
Neon-Desk/lib/task-notification-entries.test.ts

252 lines
7.8 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { buildTaskNotification } from '@/lib/server/task-notifications';
import {
buildNotificationEntries,
notificationEntrySignature,
type FilingSyncBatchState
} from '@/lib/task-notification-entries';
import type { Task } from '@/lib/types';
function makeTask(overrides: Partial<Omit<Task, 'notification'>> = {}): Task {
const task = {
id: 'task-1',
user_id: 'user-1',
task_type: 'sync_filings',
status: 'running',
stage: 'sync.extract_taxonomy',
stage_detail: 'Extracting taxonomy for NVDA',
stage_context: {
progress: {
current: 2,
total: 5,
unit: 'filings'
},
counters: {
fetched: 5,
inserted: 2,
updated: 1,
hydrated: 1,
failed: 0
},
subject: {
ticker: 'NVDA',
accessionNumber: '0000000000-26-000001'
}
},
resource_key: 'sync_filings:NVDA',
notification_read_at: null,
notification_silenced_at: null,
priority: 50,
payload: {
ticker: 'NVDA',
limit: 20
},
result: null,
error: null,
attempts: 1,
max_attempts: 3,
workflow_run_id: 'run-1',
created_at: '2026-03-14T10:00:00.000Z',
updated_at: '2026-03-14T10:05:00.000Z',
finished_at: null,
...overrides
} satisfies Omit<Task, 'notification'>;
return {
...task,
notification: buildTaskNotification(task)
};
}
function batchState(overrides: Partial<FilingSyncBatchState> = {}): FilingSyncBatchState {
return {
active: true,
taskIds: ['task-1', 'task-2'],
latestTaskId: 'task-2',
startedAt: '2026-03-14T10:00:00.000Z',
finishedAt: null,
terminalVisible: false,
...overrides
};
}
describe('task notification entries', () => {
it('collapses multiple active filing sync tasks into one aggregate entry', () => {
const first = makeTask();
const second = makeTask({
id: 'task-2',
resource_key: 'sync_filings:MSFT',
payload: { ticker: 'MSFT', limit: 20 },
stage_context: {
progress: { current: 3, total: 5, unit: 'filings' },
counters: { fetched: 4, inserted: 1, updated: 2, hydrated: 0, failed: 0 },
subject: { ticker: 'MSFT', accessionNumber: '0000000000-26-000002' }
},
updated_at: '2026-03-14T10:06:00.000Z'
});
const entries = buildNotificationEntries({
activeTasks: [first, second],
finishedTasks: [],
filingSyncBatch: batchState()
});
const filingEntries = entries.filter((entry) => entry.kind === 'filing_sync_batch');
expect(filingEntries).toHaveLength(1);
expect(filingEntries[0]).toMatchObject({
id: 'filing-sync:active',
status: 'running',
statusLine: 'Syncing filings for 2 tickers',
detailLine: '2 running • 0 queued',
primaryTaskId: 'task-2'
});
expect(filingEntries[0]?.stats).toEqual([
{ label: 'Fetched', value: '9' },
{ label: 'Inserted', value: '3' },
{ label: 'Updated', value: '3' },
{ label: 'Hydrated', value: '1' },
{ label: 'Failed', value: '0' }
]);
});
it('builds active filing sync detail from mixed queued and running tasks', () => {
const running = makeTask();
const queued = makeTask({
id: 'task-2',
status: 'queued',
stage: 'queued',
stage_detail: 'Queued for filings sync',
resource_key: 'sync_filings:AAPL',
payload: { ticker: 'AAPL', limit: 20 },
stage_context: {
progress: { current: 0, total: 5, unit: 'filings' },
counters: { fetched: 0, inserted: 0, updated: 0, hydrated: 0, failed: 0 },
subject: { ticker: 'AAPL', accessionNumber: '0000000000-26-000003' }
},
updated_at: '2026-03-14T10:04:00.000Z'
});
const entry = buildNotificationEntries({
activeTasks: [running, queued],
finishedTasks: [],
filingSyncBatch: batchState()
}).find((candidate) => candidate.kind === 'filing_sync_batch');
expect(entry?.detailLine).toBe('1 running • 1 queued');
});
it('keeps one terminal filing sync summary for mixed completion outcomes', () => {
const completed = makeTask({
status: 'completed',
stage: 'completed',
stage_detail: 'Completed sync for NVDA',
result: {
ticker: 'NVDA',
fetched: 5,
inserted: 2,
updated: 1,
taxonomySnapshotsHydrated: 1,
taxonomySnapshotsFailed: 0
},
finished_at: '2026-03-14T10:07:00.000Z'
});
const failed = makeTask({
id: 'task-2',
status: 'failed',
stage: 'sync.persist_filings',
stage_detail: 'Persist failed for MSFT',
error: 'Persist failed for MSFT',
resource_key: 'sync_filings:MSFT',
payload: { ticker: 'MSFT', limit: 20 },
stage_context: {
progress: { current: 5, total: 5, unit: 'filings' },
counters: { fetched: 5, inserted: 0, updated: 0, hydrated: 0, failed: 1 },
subject: { ticker: 'MSFT', accessionNumber: '0000000000-26-000002' }
},
finished_at: '2026-03-14T10:08:00.000Z',
updated_at: '2026-03-14T10:08:00.000Z'
});
const entry = buildNotificationEntries({
activeTasks: [],
finishedTasks: [completed, failed],
filingSyncBatch: batchState({
active: false,
terminalVisible: true,
finishedAt: '2026-03-14T10:08:00.000Z'
})
}).find((candidate) => candidate.kind === 'filing_sync_batch');
expect(entry).toMatchObject({
status: 'failed',
statusLine: 'Filing sync finished with issues',
detailLine: '2 tickers processed • 1 failed'
});
});
it('leaves non-sync tasks ungrouped', () => {
const sync = makeTask();
const refresh = makeTask({
id: 'task-3',
task_type: 'refresh_prices',
resource_key: 'refresh_prices:portfolio',
payload: {},
stage: 'refresh.fetch_quotes',
stage_detail: 'Fetching quotes',
stage_context: {
progress: { current: 3, total: 4, unit: 'tickers' },
counters: { updatedCount: 2, holdings: 4 },
subject: { ticker: 'NVDA' }
}
});
const entries = buildNotificationEntries({
activeTasks: [sync, refresh],
finishedTasks: [],
filingSyncBatch: batchState({ taskIds: ['task-1'] })
});
expect(entries.filter((entry) => entry.kind === 'single')).toHaveLength(1);
expect(entries.some((entry) => entry.id === 'task-3')).toBe(true);
expect(entries.some((entry) => entry.kind === 'filing_sync_batch')).toBe(true);
});
it('ignores noisy filing sync detail-only changes in the aggregate signature', () => {
const first = makeTask();
const second = makeTask({
id: 'task-2',
resource_key: 'sync_filings:MSFT',
payload: { ticker: 'MSFT', limit: 20 },
stage_detail: 'Extracting taxonomy for MSFT',
stage_context: {
progress: { current: 2, total: 5, unit: 'filings' },
counters: { fetched: 4, inserted: 1, updated: 2, hydrated: 0, failed: 0 },
subject: { ticker: 'MSFT', accessionNumber: '0000000000-26-000002' }
},
updated_at: '2026-03-14T10:06:00.000Z'
});
const original = buildNotificationEntries({
activeTasks: [first, second],
finishedTasks: [],
filingSyncBatch: batchState()
}).find((entry) => entry.kind === 'filing_sync_batch');
const { notification, ...secondCore } = second;
void notification;
const noisyUpdate = makeTask({
...secondCore,
stage_detail: 'Extracting taxonomy for MSFT filing 2/5'
});
const updated = buildNotificationEntries({
activeTasks: [first, noisyUpdate],
finishedTasks: [],
filingSyncBatch: batchState()
}).find((entry) => entry.kind === 'filing_sync_batch');
expect(original).toBeTruthy();
expect(updated).toBeTruthy();
expect(notificationEntrySignature(original!)).toBe(notificationEntrySignature(updated!));
});
});