import { describe, it, expect } from 'vitest'; import { mergeTimeline, type TimelineEntry } from './phishing-timeline'; describe('mergeTimeline', () => { const reports = [ { createdAt: '2026-07-15T00:00:00.000Z', reportId: 'report-1', ticketNumber: 'T-0001', companyName: 'Acme' }, { createdAt: '2026-07-15T04:00:00.000Z', reportId: 'report-2', ticketNumber: 'T-0002', companyName: 'Acme' }, ]; const classifications = [ { createdAt: '2026-07-15T02:00:00.000Z', verdict: 'THREAT', confidence: '85' }, ]; const auditEvents = [ { createdAt: '2026-07-15T01:00:00.000Z', eventType: 'remediation_approved', actor: 'lorentz@wulfconsulting.com', payload: { actions: ['block_sender'] } }, { createdAt: '2026-07-15T05:00:00.000Z', eventType: 'remediation_completed', actor: null, payload: {} }, ]; it('returns a single array sorted ascending by `at` across all 3 sources', () => { const result = mergeTimeline(reports, classifications, auditEvents); expect(result).toHaveLength(5); const timestamps = result.map((entry) => new Date(entry.at).getTime()); for (let i = 1; i < timestamps.length; i++) { expect(timestamps[i]).toBeGreaterThanOrEqual(timestamps[i - 1]); } }); it('output length equals the sum of the three input array lengths', () => { const result = mergeTimeline(reports, classifications, auditEvents); expect(result).toHaveLength(reports.length + classifications.length + auditEvents.length); }); it('carries a discriminant `kind` and the source-specific fields for each entry', () => { const result = mergeTimeline(reports, classifications, auditEvents); const reportEntry = result.find((e) => e.kind === 'report' && e.reportId === 'report-1'); expect(reportEntry).toMatchObject({ kind: 'report', reportId: 'report-1', ticketNumber: 'T-0001', companyName: 'Acme' }); const classificationEntry = result.find((e) => e.kind === 'classification'); expect(classificationEntry).toMatchObject({ kind: 'classification', verdict: 'THREAT', confidence: '85' }); const auditEntry = result.find((e) => e.kind === 'audit' && e.eventType === 'remediation_approved'); expect(auditEntry).toMatchObject({ kind: 'audit', eventType: 'remediation_approved', actor: 'lorentz@wulfconsulting.com', payload: { actions: ['block_sender'] }, }); }); it('preserves a stable tie-break order (report, then classification, then audit) on identical timestamps and does not throw', () => { const tiedAt = '2026-07-15T10:00:00.000Z'; const tiedReports = [{ createdAt: tiedAt, reportId: 'r', ticketNumber: 'T', companyName: 'C' }]; const tiedClassifications = [{ createdAt: tiedAt, verdict: 'SPAM', confidence: '50' }]; const tiedAudit = [{ createdAt: tiedAt, eventType: 'campaign_marked_false_positive', actor: null, payload: {} }]; let result: TimelineEntry[] = []; expect(() => { result = mergeTimeline(tiedReports, tiedClassifications, tiedAudit); }).not.toThrow(); expect(result.map((e) => e.kind)).toEqual(['report', 'classification', 'audit']); }); });