From 37f6657839c44e5214198bacbced4c9221942280 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 16 Jul 2026 14:27:59 -0400 Subject: [PATCH] test(22-01): add failing test for mergeTimeline - Covers ascending chronological sort across reports/classifications/audit - Asserts discriminant kind + source fields per variant, plus stable tie-break order --- lib/services/phishing-timeline.test.ts | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/services/phishing-timeline.test.ts diff --git a/lib/services/phishing-timeline.test.ts b/lib/services/phishing-timeline.test.ts new file mode 100644 index 0000000..9752c9f --- /dev/null +++ b/lib/services/phishing-timeline.test.ts @@ -0,0 +1,63 @@ +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']); + }); +});