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
This commit is contained in:
lorentz 2026-07-16 14:27:59 -04:00
parent 86cffc45b8
commit 37f6657839

View file

@ -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']);
});
});