import { describe, it, expect, beforeAll } from 'vitest' import * as db from '../src/database.js' const pubkey = 'race-test-' + Date.now() const email = 'race-test-' + Date.now() + '@example.com' let sub: any = null let since = 0 const eventA_id = 'race-event-a-' + Date.now() const eventB_id = 'race-event-b-' + Date.now() // Captured after the initial fetch, before Event B is inserted let fetchedBeforeB: string[] = [] function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } describe('Event-arrival race in digest job', () => { beforeAll(async () => { await db.migrate() // Create and confirm subscription const s = await db.insertSubscription(pubkey, email, 'daily') expect(s).toBeTruthy() const confirmed = await db.confirmSubscription(s.key) expect(confirmed).toBeTruthy() sub = confirmed // Set last_digest_at to 60 seconds ago (the "since" value runJob would use) since = Math.floor(Date.now() / 1000) - 60 await db.updateLastDigestAt(sub.id, since) // Wait 1.1s so event received_at timestamps are strictly > since await sleep(1100) }) it('stores Event A (triggers digest)', async () => { const eventA = { id: eventA_id, kind: 1, pubkey: 'abc', content: 'event A content', created_at: Math.floor(Date.now() / 1000), tags: [], } const storedA = await db.insertEvent(eventA_id, sub.id, eventA, 'wss://relay.damus.io') expect(storedA).toBe(true) }) // Fetch events BEFORE Event B is inserted (simulating runJob's fetch // before a /notify arrives during the digest send). Save the IDs we // fetched so we delete exactly those later. it('fetches events and captures IDs (simulating runJob fetch)', async () => { const fetched = await db.getEventsForSubscription(sub.id, since) expect(fetched.some((e: any) => e.id === eventA_id)).toBe(true) fetchedBeforeB = fetched.map((e: any) => e.id) }) it('stores Event B AFTER fetch (simulating arrival during digest send)', async () => { await sleep(100) const eventB = { id: eventB_id, kind: 1, pubkey: 'def', content: 'event B content — arrived during send', created_at: Math.floor(Date.now() / 1000), tags: [], } const storedB = await db.insertEvent(eventB_id, sub.id, eventB, 'wss://relay.damus.io') expect(storedB).toBe(true) }) // Delete using the IDs captured before Event B was inserted. // This simulates the fix: deleteEventsByIds, not timestamp-based delete. it('deletes only previously-fetched event IDs (the fix) and leaves event B', async () => { await db.deleteEventsByIds(sub.id, fetchedBeforeB) // Event B must survive (it arrived after fetch and was never sent) const remaining = await db.getEventsForSubscription(sub.id, since - 10) const eventB_survived = remaining.some((e: any) => e.id === eventB_id) expect(eventB_survived).toBe(true) }) it('Event A is deleted (it was fetched and sent)', async () => { const remaining = await db.getEventsForSubscription(sub.id, since - 10) const eventA_survived = remaining.some((e: any) => e.id === eventA_id) expect(eventA_survived).toBe(false) }) })