import { describe, it, expect, vi, beforeEach } from 'vitest' import type { TrustedEvent } from '@welshman/util' // ── Shared state accessible from both vi.mock factories and test body ────── const mockRepo = vi.hoisted(() => { const events: TrustedEvent[] = [] return { events, // shared mutable array query: vi.fn((filters: any[]) => { return events.filter(e => { for (const f of filters) { // #e filter: event must have an 'e' tag whose value matches one of the filter values if (f['#e']) { const eTagVals = e.tags.filter(t => t[0] === 'e').map(t => t[1]) if (!f['#e'].some((id: string) => eTagVals.includes(id))) return false } // kinds filter if (f.kinds && !f.kinds.includes(e.kind)) return false } return true }) }), publish: vi.fn((event: TrustedEvent) => { events.push(event) return true }), } }) // ── Mocks (hoisted before imports) ───────────────────────────────────────── // ── Mock profiles map (pre-populated so waitForProfile doesn't time out) ── const mockProfilesMap = vi.hoisted(() => new Map()) vi.mock('../src/repository.js', () => ({ profilesByPubkey: { get: () => mockProfilesMap }, loadProfile: vi.fn().mockResolvedValue(undefined), repository: mockRepo, })) vi.mock('../src/util.js', () => { const createElementMock = vi.fn().mockImplementation((tagName: string) => { const el: any = { tagName, children: [], innerText: '' } el.appendChild = (child: any) => { el.children.push(child) } el.toString = () => `<${tagName}>${el.innerText}${el.children.map((c: any) => c.toString()).join('')}` return el }) return { displayDuration: vi.fn().mockReturnValue('1 hour'), createElement: createElementMock, } }) // Captured digest parameters (set by the mailer mock) let lastDigestParams: Record | undefined vi.mock('../src/mailer.js', () => ({ sendDigest: vi.fn((_sub: any, variables: Record) => { lastDigestParams = variables }), })) // ── Imports (after mocks) ────────────────────────────────────────────────── import { Digest } from '../src/digest.js' import type { Subscription } from '../src/alert.js' // Valid 64-char hex strings for nostr IDs and pubkeys const PARENT_ID = 'aaa' + 'a'.repeat(61) // 64 hex chars const REPLY_ID = 'bbb' + 'b'.repeat(61) const REACTION_ID = 'ccc' + 'c'.repeat(61) const PARENT_PK = 'ddd' + 'd'.repeat(61) const REPLIER_PK = 'eee' + 'e'.repeat(61) const REACTER_PK = 'fff' + 'f'.repeat(61) function makeEvent(overrides: Partial): TrustedEvent { const eid = overrides.id || 'a'.repeat(64) return { id: eid, kind: 1, pubkey: PARENT_PK, created_at: Math.floor(Date.now() / 1000), tags: [], content: 'test content', ...overrides, id: eid, } as TrustedEvent } describe('digest reply/reaction stats', () => { let sub: Subscription beforeEach(() => { // Reset shared state mockRepo.events.length = 0 lastDigestParams = undefined sub = { id: 'sub-1', key: 'key-1', pubkey: PARENT_PK, email: 'test@example.com', frequency: 'daily', created_at: Math.floor(Date.now() / 1000) - 3600, confirmed_at: Math.floor(Date.now() / 1000) - 3600, } }) it('should count replies and reactions loaded from repository context', async () => { // Create a parent event const parentEvent = makeEvent({ id: PARENT_ID, kind: 1, pubkey: PARENT_PK, content: 'Hello world, this is the parent event', created_at: Math.floor(Date.now() / 1000) - 600, }) // Create a reply event referencing the parent via an 'e' tag const replyEvent = makeEvent({ id: REPLY_ID, kind: 1, pubkey: REPLIER_PK, content: 'This is a reply to the parent', created_at: Math.floor(Date.now() / 1000) - 500, tags: [['e', PARENT_ID, '', 'root']], }) // Create a reaction event (kind 7 = REACTION) const reactionEvent = makeEvent({ id: REACTION_ID, kind: 7, pubkey: REACTER_PK, content: '+', created_at: Math.floor(Date.now() / 1000) - 400, tags: [['e', PARENT_ID, '', 'root']], }) // Publish reply/reaction events to the mock repository so the fix can find them mockRepo.publish(replyEvent) mockRepo.publish(reactionEvent) // Pre-populate profiles so waitForProfile resolves immediately mockProfilesMap.set(PARENT_PK, { pubkey: PARENT_PK, name: 'parent-user', picture: '' }) mockProfilesMap.set(REPLIER_PK, { pubkey: REPLIER_PK, name: 'replier', picture: '' }) mockProfilesMap.set(REACTER_PK, { pubkey: REACTER_PK, name: 'reacter', picture: '' }) const storedEvents = [ { id: 'se-1', event: parentEvent, relay: 'wss://relay.example.com' }, ] const digest = new Digest(sub) await digest.sendFromStoredEvents(storedEvents) // sendDigest should have been called with the template parameters expect(lastDigestParams).toBeDefined() const latest = lastDigestParams!.Latest expect(latest.length).toBeGreaterThanOrEqual(1) const parentEntry = latest[0] // RelayUrl should be the relay domain stripped of protocol and trailing slash expect(parentEntry.RelayUrl).toBe('relay.example.com') }) })