import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockQuery, mockConnect } = vi.hoisted(() => ({ mockQuery: vi.fn(), mockConnect: vi.fn(), })); vi.mock('../db/index.js', () => ({ query: mockQuery, getPool: () => ({ connect: mockConnect }), })); import { recordDelivery, markProcessing, markDone, markFailed, resetStaleProcessing, } from '../db/ingest_events.dao.js'; describe('ingest_events.dao', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('recordDelivery', () => { it('should return the new row id for a delivery that has not been seen', async () => { mockQuery.mockResolvedValue({ rows: [{ id: 42 }] }); const result = await recordDelivery({ provider: 'slack', externalId: 'evt_1' }); expect(result).toBe(42); }); it('should return null when the delivery conflicts with an existing row', async () => { mockQuery.mockResolvedValue({ rows: [] }); const result = await recordDelivery({ provider: 'slack', externalId: 'evt_1' }); expect(result).toBeNull(); }); it('should insert with ON CONFLICT DO NOTHING on the provider and external id', async () => { mockQuery.mockResolvedValue({ rows: [{ id: 1 }] }); await recordDelivery({ provider: 'linear', externalId: 'evt_2' }); const [sql] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('INSERT INTO ingest_events'); expect(sql).toContain('ON CONFLICT (provider, external_id) DO NOTHING'); expect(sql).toContain('RETURNING id'); }); it('should bind the provider, external id and integration id', async () => { mockQuery.mockResolvedValue({ rows: [{ id: 7 }] }); await recordDelivery({ provider: 'github', externalId: 'evt_3', integrationId: 12 }); const [, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(params).toEqual(['github', 'evt_3', 12]); }); it('should bind null when no integration id is supplied', async () => { mockQuery.mockResolvedValue({ rows: [{ id: 8 }] }); await recordDelivery({ provider: 'rest', externalId: 'evt_4' }); const [, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(params[2]).toBeNull(); }); }); describe('markProcessing', () => { it('should set the status to processing and increment attempts', async () => { mockQuery.mockResolvedValue({ rows: [] }); await markProcessing(5); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain("status = 'processing'"); expect(sql).toContain('attempts = attempts + 1'); expect(params).toEqual([5]); }); }); describe('markDone', () => { it('should set the status to done and clear the last error', async () => { mockQuery.mockResolvedValue({ rows: [] }); await markDone(9); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain("status = 'done'"); expect(sql).toContain('last_error = NULL'); expect(params).toEqual([9]); }); }); describe('markFailed', () => { it('should store the failure message against the row', async () => { mockQuery.mockResolvedValue({ rows: [] }); await markFailed(3, 'normalizer threw'); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain("status = 'failed'"); expect(sql).toContain('last_error = $2'); expect(params).toEqual([3, 'normalizer threw']); }); it('should truncate an over-long error to 2000 characters', async () => { mockQuery.mockResolvedValue({ rows: [] }); await markFailed(3, 'x'.repeat(5000)); const [, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(params[1]).toBe('x'.repeat(2000)); }); }); describe('resetStaleProcessing', () => { it('should return the number of rows returned to pending', async () => { mockQuery.mockResolvedValue({ rows: [], rowCount: 4 }); const result = await resetStaleProcessing(30_000); expect(result).toBe(4); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain("SET status = 'pending'"); expect(sql).toContain("status = 'processing'"); expect(params).toEqual(['30000']); }); it('should return 0 when the driver reports a null row count', async () => { mockQuery.mockResolvedValue({ rows: [], rowCount: null }); const result = await resetStaleProcessing(30_000); expect(result).toBe(0); }); }); });