import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import Database from 'better-sqlite3'; import { runMigrations } from '../src/database/client.js'; import { DeliveryOutboxRepository, MessagesRepository, PostsRepository } from '../src/database/repositories.js'; describe('DeliveryOutboxRepository', () => { let db: Database.Database; let outbox: DeliveryOutboxRepository; beforeEach(() => { db = new Database(':memory:'); db.pragma('foreign_keys = ON'); runMigrations(db); new PostsRepository(db).insert({ id: 'post-1', subreddit: 'test', author: 'author', title: 'title', imageUrl: null, processedAt: 1, }); outbox = new DeliveryOutboxRepository(db); }); afterEach(() => db.close()); it('deduplicates discovery by idempotency key without creating another delivery', () => { const first = outbox.discover({ postId: 'post-1', recipient: 'recipient', subject: 'subject', message: 'message', idempotencyKey: 'post-1:v1', now: 100, }); const second = outbox.discover({ postId: 'post-1', recipient: 'recipient', subject: 'changed', message: 'changed', idempotencyKey: 'post-1:v1', now: 200, }); expect(second).toEqual(first); expect(outbox.count()).toBe(1); expect(outbox.get(first.id)).toMatchObject({ status: 'discovered', subject: 'subject' }); }); it('recovers an expired lease and allows exactly one worker to claim the prepared delivery', () => { const delivery = outbox.discover({ postId: 'post-1', recipient: 'recipient', subject: 'subject', message: 'message', idempotencyKey: 'post-1:v1', now: 100, }); outbox.markPrepared(delivery.id, 110); expect(outbox.claimNext('worker-a', 50, 120)).toMatchObject({ id: delivery.id, status: 'sending', leaseOwner: 'worker-a' }); expect(outbox.claimNext('worker-b', 50, 121)).toBeNull(); expect(outbox.recoverExpiredLeases(171)).toBe(1); expect(outbox.claimNext('worker-b', 50, 172)).toMatchObject({ id: delivery.id, leaseOwner: 'worker-b', attemptCount: 2 }); }); it('claims a specific prepared delivery without taking another queued item', () => { new PostsRepository(db).insert({ id: 'post-2', subreddit: 'test', author: 'author-2', title: 'title', imageUrl: null, processedAt: 2 }); const first = outbox.discover({ postId: 'post-1', recipient: 'a', subject: 's', message: 'm', idempotencyKey: 'first', now: 1 }); const second = outbox.discover({ postId: 'post-2', recipient: 'b', subject: 's', message: 'm', idempotencyKey: 'second', now: 2 }); outbox.markPrepared(first.id, 3); outbox.markPrepared(second.id, 3); expect(outbox.claim(first.id, 'worker', 100, 4)).toMatchObject({ id: first.id, status: 'sending' }); expect(outbox.get(second.id)).toMatchObject({ status: 'prepared' }); }); it('only completes a claimed delivery for its current lease owner', () => { const delivery = outbox.discover({ postId: 'post-1', recipient: 'recipient', subject: 'subject', message: 'message', idempotencyKey: 'post-1:v1', now: 100, }); outbox.markPrepared(delivery.id, 110); outbox.claimNext('worker-a', 50, 120); const sentMessageId = new MessagesRepository(db).insert({ postId: 'post-1', recipient: 'recipient', subject: 'subject', message: 'message', sentAt: 125, }); expect(outbox.markSent(delivery.id, 'worker-b', sentMessageId, 130)).toBe(false); expect(outbox.markSent(delivery.id, 'worker-a', sentMessageId, 130)).toBe(true); expect(outbox.get(delivery.id)).toMatchObject({ status: 'sent', sentMessageId, leaseOwner: null }); }); });