Files
ims/tests/lib/notifications/capa-escalation.test.ts
T
adminandClaude Sonnet 4.6 25f923f530 feat(db): phase 4 group 1 — lib/ settings + notifications to Drizzle
Convert lib/settings.ts, lib/notifications/{in-app,email,capa-escalation,
effectiveness-recheck}.ts from Supabase PostgREST to Drizzle asAdmin queries.
Drop supabase arg from all call sites in app/api/ and cron routes. Rewrite
notification unit tests to mock @/lib/db/with-user instead of SupabaseClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 16:37:51 +08:00

120 lines
3.9 KiB
TypeScript

// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getEscalationThreshold, escalateOverdueCapa } from '@/lib/notifications/capa-escalation'
vi.mock('resend', () => {
const sendMock = vi.fn().mockResolvedValue({ data: { id: 'email-id' }, error: null })
function ResendMock() {
return { emails: { send: sendMock } }
}
return { Resend: ResendMock }
})
vi.mock('@/lib/notifications/whatsapp', () => ({
sendWhatsAppMessage: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/lib/settings', () => ({
getApiKey: vi.fn().mockResolvedValue('test-cred'),
}))
vi.mock('@/lib/notifications/in-app', () => ({
createInAppNotifications: vi.fn().mockResolvedValue({ created: 1 }),
}))
// At top of file, before other vi.mock calls
let mockAsAdminImpl: (fn: (db: unknown) => unknown) => unknown
vi.mock('@/lib/db/with-user', () => ({
asAdmin: vi.fn().mockImplementation((fn: (db: unknown) => unknown) => mockAsAdminImpl(fn)),
}))
describe('getEscalationThreshold', () => {
function daysFromNow(n: number): string {
const d = new Date()
d.setDate(d.getDate() + n)
return d.toISOString().split('T')[0]
}
it('returns warning_3d when due in 3 days', () => {
expect(getEscalationThreshold(daysFromNow(3))).toBe('warning_3d')
})
it('returns due_today when due today', () => {
expect(getEscalationThreshold(daysFromNow(0))).toBe('due_today')
})
it('returns overdue_3d when 3 days past due', () => {
expect(getEscalationThreshold(daysFromNow(-3))).toBe('overdue_3d')
})
it('returns overdue_7d when 7 days past due', () => {
expect(getEscalationThreshold(daysFromNow(-7))).toBe('overdue_7d')
})
it('returns null for 2 days before due (no threshold)', () => {
expect(getEscalationThreshold(daysFromNow(2))).toBeNull()
})
it('returns null for 4 days before due', () => {
expect(getEscalationThreshold(daysFromNow(4))).toBeNull()
})
})
describe('escalateOverdueCapa', () => {
function daysFromNow(n: number): string {
const d = new Date()
d.setDate(d.getDate() + n)
return d.toISOString().split('T')[0]
}
beforeEach(() => { vi.clearAllMocks() })
it('returns notified:0 when no active capas', async () => {
let call = 0
mockAsAdminImpl = (fn) => {
call++
if (call === 1) return fn({ select: () => ({ from: () => ({ leftJoin: () => ({ leftJoin: () => ({ where: () => [] }) }) }) }) })
return fn({})
}
const { notified } = await escalateOverdueCapa()
expect(notified).toBe(0)
})
it('skips capa with no threshold match', async () => {
let call = 0
mockAsAdminImpl = (fn) => {
call++
if (call === 1) {
// Return capa due in 2 days (no threshold)
return Promise.resolve([{
id: 'c1', description: 'Fix it', dueDate: daysFromNow(2),
incidentId: 'i1', ownerUserId: 'u1',
incidentRef: 'SITE-202507-0001', siteId: 's1',
ownerEmail: 'owner@example.com', ownerName: 'Owner', ownerPhone: null,
}])
}
return Promise.resolve([])
}
const { notified } = await escalateOverdueCapa()
expect(notified).toBe(0)
})
it('sends email and WhatsApp for overdue_3d capa with phone', async () => {
const { sendWhatsAppMessage } = await import('@/lib/notifications/whatsapp')
let call = 0
mockAsAdminImpl = (fn) => {
call++
if (call === 1) return Promise.resolve([{
id: 'c1', description: 'Fix it', dueDate: daysFromNow(-3),
incidentId: 'i1', ownerUserId: 'u1',
incidentRef: 'SITE-202507-0001', siteId: 's1',
ownerEmail: 'owner@example.com', ownerName: 'Owner', ownerPhone: '+60123456789',
}])
return Promise.resolve([]) // alreadySent = [], hseUsers = [], insert void
}
const { notified } = await escalateOverdueCapa()
expect(notified).toBe(1)
expect(sendWhatsAppMessage).toHaveBeenCalled()
})
})