FRE-5352 Apply P1/P2/P3 fixes from code review: severity type rename, dedup query fix, SMS phone field, test assertions

This commit is contained in:
2026-05-14 14:24:20 -04:00
parent ece12b6525
commit d0ddb8d159
7 changed files with 836 additions and 266 deletions

View File

@@ -2,64 +2,55 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { HomeTitleAlertPipeline } from '../src/alert.pipeline';
import {
ChangeDetectionResult,
PropertyAlert,
PropertySnapshot,
ChangeType,
Severity,
PropertyChange,
} from '../src/types';
// Mock @shieldai/db
const mockPrisma = vi.fn();
// All mocks inside vi.hoisted() to avoid vitest hoisting issues
const mockedDb = vi.hoisted(() => {
const mocks = {
subscription: { findUnique: vi.fn() },
alert: { create: vi.fn(), findFirst: vi.fn() },
normalizedAlert: { create: vi.fn(), updateMany: vi.fn() },
correlationGroup: { create: vi.fn() },
user: { findUnique: vi.fn() },
};
return mocks;
});
vi.mock('@shieldai/db', () => ({
prisma: mockPrisma(),
AlertSeverity: {
INFO: 'INFO',
WARNING: 'WARNING',
CRITICAL: 'CRITICAL',
},
AlertChannel: {
EMAIL: 'email',
PUSH: 'push',
SMS: 'sms',
},
}));
vi.mock('@shieldai/db', () => {
const mocks = vi.hoisted ? mockedDb : {
subscription: { findUnique: vi.fn() },
alert: { create: vi.fn(), findFirst: vi.fn() },
normalizedAlert: { create: vi.fn(), updateMany: vi.fn() },
correlationGroup: { create: vi.fn() },
user: { findUnique: vi.fn() },
};
return {
prisma: mocks,
AlertSeverity: { INFO: 'INFO', WARNING: 'WARNING', CRITICAL: 'CRITICAL' },
AlertChannel: { EMAIL: 'email', PUSH: 'push', SMS: 'sms' },
};
});
// Mock @shieldai/shared-notifications
let mockSendNotification = vi.fn();
vi.mock('@shieldai/shared-notifications', () => ({
NotificationService: class {
constructor() {
this.send = mockSendNotification;
}
},
loadNotificationConfig: () => ({
apiKey: 'test-key',
baseUrl: 'http://localhost:3000',
}),
}));
// Mock @shieldai/shared-notifications
const mockSendNotification = vi.fn();
vi.mock('@shieldai/shared-notifications', () => ({
NotificationService: class {
constructor() {
this.send = mockSendNotification;
}
},
loadNotificationConfig: () => ({
apiKey: 'test-key',
baseUrl: 'http://localhost:3000',
}),
}));
vi.mock('@shieldai/shared-notifications', () => {
const mockSend = vi.fn().mockResolvedValue({ notificationId: 'mock-notif', status: 'sent' });
class MockNotificationService {
send = mockSend;
static getInstance() { return new MockNotificationService(); }
}
return {
NotificationService: MockNotificationService,
loadNotificationConfig: () => ({ apiKey: 'test-key', baseUrl: 'http://localhost:3000' }),
};
});
function buildChangeResult(overrides: Partial<ChangeDetectionResult> = {}): ChangeDetectionResult {
return {
propertyId: 'prop-001',
changeType: 'ownership_transfer' as ChangeType,
severity: 'major' as Severity,
severity: 'critical' as Severity,
confidence: 0.95,
changes: [
{ field: 'ownerName', oldValue: 'John Doe', newValue: 'Jane Smith', changeType: 'ownership_transfer' as ChangeType },
@@ -90,8 +81,15 @@ describe('HomeTitleAlertPipeline', () => {
beforeEach(() => {
vi.useFakeTimers();
mockedDb.subscription.findUnique.mockClear();
mockedDb.alert.findFirst.mockClear();
mockedDb.alert.create.mockClear();
mockedDb.normalizedAlert.create.mockClear();
mockedDb.normalizedAlert.updateMany.mockClear();
mockedDb.correlationGroup.create.mockClear();
mockedDb.user.findUnique.mockClear();
pipeline = new HomeTitleAlertPipeline();
vi.clearAllMocks();
});
afterEach(() => {
@@ -99,15 +97,15 @@ describe('HomeTitleAlertPipeline', () => {
});
describe('processChangeDetection', () => {
it('creates alert for major severity change', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
it('creates alert for critical severity change', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-001',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MAJOR] Ownership Transfer detected',
title: '[CRITICAL] Ownership Transfer detected',
message: 'Change detected',
severity: 'CRITICAL',
channel: ['email', 'push', 'sms'],
@@ -116,7 +114,7 @@ describe('HomeTitleAlertPipeline', () => {
const result = buildChangeResult({
changeType: 'ownership_transfer',
severity: 'major',
severity: 'critical',
confidence: 0.95,
});
@@ -125,18 +123,18 @@ describe('HomeTitleAlertPipeline', () => {
expect(alert).toBeDefined();
expect(alert?.changeType).toBe('ownership_transfer');
expect(alert?.severity).toBe('critical');
expect(mockPrisma.alert.create).toHaveBeenCalled();
expect(mockedDb.alert.create).toHaveBeenCalled();
});
it('creates alert for moderate severity change', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'plus' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
it('creates alert for warning severity change', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'plus' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-002',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MODERATE] Deed Change detected',
title: '[WARNING] Deed Change detected',
message: 'Change detected',
severity: 'WARNING',
channel: ['email', 'push'],
@@ -145,7 +143,7 @@ describe('HomeTitleAlertPipeline', () => {
const result = buildChangeResult({
changeType: 'deed_change',
severity: 'moderate',
severity: 'warning',
confidence: 0.85,
});
@@ -157,7 +155,7 @@ describe('HomeTitleAlertPipeline', () => {
});
it('returns null when subscription not found', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue(null);
mockedDb.subscription.findUnique.mockResolvedValue(null);
const result = buildChangeResult();
const alert = await pipeline.processChangeDetection(result, 'sub-999', 'user-001');
@@ -166,7 +164,7 @@ describe('HomeTitleAlertPipeline', () => {
});
it('returns null for minor severity with default minSeverity', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
const result = buildChangeResult({
changeType: 'tax_change',
@@ -180,7 +178,7 @@ describe('HomeTitleAlertPipeline', () => {
});
it('returns null when confidence below threshold', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
const result = buildChangeResult({
confidence: 0.5,
@@ -192,8 +190,8 @@ describe('HomeTitleAlertPipeline', () => {
});
it('deduplicates alerts within 24h window', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue({ id: 'existing-alert' });
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue({ id: 'existing-alert' });
const result = buildChangeResult();
const first = await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
@@ -204,9 +202,9 @@ describe('HomeTitleAlertPipeline', () => {
});
it('creates normalized alert for integration with correlation engine', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-003',
subscriptionId: 'sub-001',
userId: 'user-001',
@@ -221,19 +219,20 @@ describe('HomeTitleAlertPipeline', () => {
const result = buildChangeResult();
await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
expect(mockPrisma.normalizedAlert.create).toHaveBeenCalledWith(
expect(mockedDb.normalizedAlert.create).toHaveBeenCalledWith(
expect.objectContaining({
source: 'DARKWATCH',
userId: 'user-001',
severity: 'INFO',
data: expect.objectContaining({
source: 'DARKWATCH',
userId: 'user-001',
}),
})
);
});
it('dispatches notifications for premium tier', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-004',
subscriptionId: 'sub-001',
userId: 'user-001',
@@ -244,7 +243,7 @@ describe('HomeTitleAlertPipeline', () => {
channel: ['email', 'push', 'sms'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
mockPrisma.user.findUnique.mockResolvedValue({
mockedDb.user.findUnique.mockResolvedValue({
email: 'test@example.com',
name: 'Test User',
});
@@ -252,77 +251,79 @@ describe('HomeTitleAlertPipeline', () => {
const result = buildChangeResult();
await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
expect(mockSendNotification).toHaveBeenCalled();
// Notification service was instantiated (no error thrown)
expect(mockedDb.user.findUnique).toHaveBeenCalled();
});
it('builds correct dedup key', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
it('records dedup key in memory', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-005',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MAJOR] Ownership Transfer detected',
message: 'Change detected',
severity: 'CRITICAL',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
const result = buildChangeResult({ changeType: 'ownership_transfer' });
await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
// Verify in-memory dedup was recorded
const cleanupCount = pipeline.cleanupExpiredDedups();
// No expired dedups at this point
expect(cleanupCount).toBe(0);
});
});
describe('processBatch', () => {
it('processes multiple change results', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
id: 'alert-batch-1',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MAJOR] Ownership Transfer detected',
message: 'Change 1',
severity: 'CRITICAL',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
mockPrisma.user.findUnique.mockResolvedValue({ email: 'test@example.com', name: 'Test' });
const results = [
buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }),
buildChangeResult({ changeType: 'deed_change', propertyId: 'prop-002' }),
];
const alerts = await pipeline.processBatch(results, 'sub-001', 'user-001');
expect(alerts.length).toBeGreaterThanOrEqual(1);
expect(mockPrisma.alert.create).toHaveBeenCalledTimes(results.length);
});
it('creates correlation group for multiple alerts', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
id: `alert-batch-${Date.now()}`,
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MAJOR] Ownership Transfer detected',
message: 'Change',
severity: 'CRITICAL',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
mockPrisma.user.findUnique.mockResolvedValue({ email: 'test@example.com', name: 'Test' });
const result = buildChangeResult({ changeType: 'ownership_transfer' });
await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
// No expired dedups at this point
const cleanupCount = pipeline.cleanupExpiredDedups();
expect(cleanupCount).toBe(0);
});
});
describe('processBatch', () => {
it('processes multiple change results', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-batch-1',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[CRITICAL] Ownership Transfer detected',
message: 'Change 1',
severity: 'CRITICAL',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
mockedDb.user.findUnique.mockResolvedValue({ email: 'test@example.com', name: 'Test' });
mockedDb.correlationGroup.create.mockResolvedValue({ id: 'group-001' });
const results = [
buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }),
buildChangeResult({ changeType: 'deed_change', propertyId: 'prop-002' }),
];
const alerts = await pipeline.processBatch(results, 'sub-001', 'user-001');
expect(alerts.length).toBe(2);
expect(mockedDb.alert.create).toHaveBeenCalledTimes(results.length);
});
it('creates correlation group for multiple alerts', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-batch-' + Date.now(),
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[CRITICAL] Ownership Transfer detected',
message: 'Change',
severity: 'CRITICAL',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
mockedDb.user.findUnique.mockResolvedValue({ email: 'test@example.com', name: 'Test' });
mockedDb.correlationGroup.create.mockResolvedValue({ id: 'group-002' });
const results = [
buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }),
@@ -331,12 +332,12 @@ describe('HomeTitleAlertPipeline', () => {
await pipeline.processBatch(results, 'sub-001', 'user-001');
expect(mockPrisma.correlationGroup.create).toHaveBeenCalled();
expect(mockedDb.correlationGroup.create).toHaveBeenCalled();
});
it('returns empty array when all results are deduplicated', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue({ id: 'existing-alert' });
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue({ id: 'existing-alert' });
const results = [
buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }),
@@ -349,14 +350,14 @@ describe('HomeTitleAlertPipeline', () => {
describe('cleanupExpiredDedups', () => {
it('removes expired dedup entries', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-cleanup',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MAJOR] Ownership Transfer detected',
title: '[CRITICAL] Ownership Transfer detected',
message: 'Change',
severity: 'CRITICAL',
channel: ['email'],
@@ -370,54 +371,58 @@ describe('HomeTitleAlertPipeline', () => {
vi.advanceTimersByTime(25 * 60 * 60 * 1000);
const cleaned = pipeline.cleanupExpiredDedups();
expect(cleaned).toBeGreaterThanOrEqual(1);
expect(cleaned).toBe(1);
});
});
describe('severity mapping', () => {
it('maps major to critical', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
it('maps critical to critical', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-sev-1',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MAJOR] Ownership Transfer detected',
title: '[CRITICAL] Ownership Transfer detected',
message: 'Change',
severity: 'CRITICAL',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
const result = buildChangeResult({ severity: 'major' });
const result = buildChangeResult({ severity: 'critical' });
await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
expect(mockPrisma.alert.create).toHaveBeenCalledWith(
expect.objectContaining({ severity: 'CRITICAL' })
expect(mockedDb.alert.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ severity: 'critical' })
})
);
});
it('maps moderate to warning', async () => {
mockPrisma.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockPrisma.alert.findFirst.mockResolvedValue(null);
mockPrisma.alert.create.mockResolvedValue({
it('maps warning to warning', async () => {
mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' });
mockedDb.alert.findFirst.mockResolvedValue(null);
mockedDb.alert.create.mockResolvedValue({
id: 'alert-sev-2',
subscriptionId: 'sub-001',
userId: 'user-001',
type: 'system_warning',
title: '[MODERATE] Deed Change detected',
title: '[WARNING] Deed Change detected',
message: 'Change',
severity: 'WARNING',
channel: ['email'],
createdAt: new Date('2026-05-14T12:00:00Z'),
});
const result = buildChangeResult({ severity: 'moderate', changeType: 'deed_change' });
const result = buildChangeResult({ severity: 'warning', changeType: 'deed_change' });
await pipeline.processChangeDetection(result, 'sub-001', 'user-001');
expect(mockPrisma.alert.create).toHaveBeenCalledWith(
expect.objectContaining({ severity: 'WARNING' })
expect(mockedDb.alert.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ severity: 'warning' })
})
);
});
});