All checks were successful
CI / checks (pull_request) Successful in 34s
Instead of setting context = events (which made repliesByParentId always empty), query the repository for events that #e-tag our matched event IDs with kinds NOTE, COMMENT, or REACTION. This gives buildParameters real reply/reaction data to count. Fixes the bug where every event in the digest email showed '0 replies / 0 reactions'. Added test/digest-reply-stats.test.ts that: - Publishes reply and reaction events into a mock repository - Calls sendFromStoredEvents with only the parent event - Asserts that the resulting digest parameters show Replies >= 1 and Reactions >= 1
167 lines
No EOL
5.6 KiB
TypeScript
167 lines
No EOL
5.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import type { TrustedEvent } from '@welshman/util'
|
|
import { spec } from '@welshman/lib'
|
|
|
|
// ── 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('')}</${tagName}>`
|
|
return el
|
|
})
|
|
return {
|
|
displayDuration: vi.fn().mockReturnValue('1 hour'),
|
|
createElement: createElementMock,
|
|
}
|
|
})
|
|
|
|
// Captured digest parameters (set by the mailer mock)
|
|
let lastDigestParams: Record<string, any> | undefined
|
|
|
|
vi.mock('../src/mailer.js', () => ({
|
|
sendDigest: vi.fn((_sub: any, variables: Record<string, any>) => {
|
|
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>): 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]
|
|
// When context includes reply/reaction events loaded from the repository,
|
|
// Replies should be >= 1 and Reactions >= 1
|
|
expect(parentEntry.Replies).toBeGreaterThanOrEqual(1)
|
|
expect(parentEntry.Reactions).toBeGreaterThanOrEqual(1)
|
|
})
|
|
}) |