Files
psf-memo/psf-memo-db/test/unit/lib/backfill-profile-recency.unit.js
T
Chris Troutner ace703831f Harden profile-last-post with mutation-killing tests
Add unit tests for the profileRecency backfill and recent-profiles read
path that kill the language-mutation survivors: status-tip confirmation
(no tip, exact tip), exclusion of addresses without a profile, replies,
and polls, malformed addrPostHeight entries, missing optional stores,
seen/height defaulting, stale-record removal, and the height/seen/address
comparator tie-breakers. Tighten the indexer recency tests the same way
and assert the set-profile text and max-size boundary.

Refresh the mutate4javascript manifests and write the gherkin-mutator
acceptance mutation metadata for the changed features.

By architect.
2026-09-20 15:29:24 -07:00

223 lines
7.6 KiB
JavaScript

import { assert } from 'chai'
import { backfillProfileRecency, partsFromAddrPostHeightKey } from '../../../src/lib/backfill-profile-recency.js'
import { FakeDb } from '../../support/level-double.js'
const ALICE = 'bitcoincash:qaddr-alice'
const BOB = 'bitcoincash:qaddr-bob'
const NOPOST = 'bitcoincash:qaddr-nopost'
function pad (height) {
return String(height).padStart(12, '0')
}
function addrPostHeightKey (addr, height, txid) {
return `${addr}:${pad(height)}:${txid}`
}
function fixtureStores () {
const profilesDb = new FakeDb([
[ALICE, { text: 'alice bio', txid: 'profile-alice' }],
[BOB, { text: 'bob bio', txid: 'profile-bob' }],
[NOPOST, { text: 'nopost bio', txid: 'profile-nopost' }]
])
const postsDb = new FakeDb([
['post-a1', { addr: ALICE, seen: 100, blockHeight: 600100 }],
['reply-a1', { addr: ALICE, seen: 150, blockHeight: 600300 }],
['post-b1', { addr: BOB, seen: 200, blockHeight: 600400 }],
['post-b2', { addr: BOB, seen: 250, blockHeight: 600500 }]
])
const addrPostHeightsDb = new FakeDb([
[addrPostHeightKey(ALICE, 600100, 'post-a1'), { txid: 'post-a1', addr: ALICE, blockHeight: 600100 }],
[addrPostHeightKey(ALICE, 600300, 'reply-a1'), { txid: 'reply-a1', addr: ALICE, blockHeight: 600300 }],
[addrPostHeightKey(ALICE, 600200, 'poll-a1'), { txid: 'poll-a1', addr: ALICE, blockHeight: 600200 }],
[addrPostHeightKey(BOB, 600400, 'post-b1'), { txid: 'post-b1', addr: BOB, blockHeight: 600400 }],
[addrPostHeightKey(BOB, 600500, 'post-b2'), { txid: 'post-b2', addr: BOB, blockHeight: 600500 }]
])
const postParentsDb = new FakeDb([
['reply-a1', { txid: 'reply-a1', parentTxid: 'post-a1' }]
])
const pollsDb = new FakeDb([
['poll-a1', { txid: 'poll-a1' }]
])
const statusDb = new FakeDb([
['status', { chainBlockHeight: 600450 }]
])
const profileRecencyDb = new FakeDb()
return { profilesDb, postsDb, addrPostHeightsDb, postParentsDb, pollsDb, statusDb, profileRecencyDb }
}
describe('#backfillProfileRecency', () => {
it('should record each profile newest confirmed qualifying post', async () => {
const stores = fixtureStores()
const result = await backfillProfileRecency(stores)
assert.equal(result.profiles, 2)
assert.deepEqual(stores.profileRecencyDb.store.get(ALICE), {
addr: ALICE, blockHeight: 600100, seen: 100
})
assert.deepEqual(stores.profileRecencyDb.store.get(BOB), {
addr: BOB, blockHeight: 600400, seen: 200
})
})
it('should not record a profile with no qualifying post', async () => {
const stores = fixtureStores()
await backfillProfileRecency(stores)
assert.isFalse(stores.profileRecencyDb.store.has(NOPOST))
})
it('should be idempotent', async () => {
const stores = fixtureStores()
await backfillProfileRecency(stores)
const afterFirst = new Map(stores.profileRecencyDb.store)
await backfillProfileRecency(stores)
assert.deepEqual(stores.profileRecencyDb.store, afterFirst)
})
it('should remove stale recency records for addresses that no longer qualify', async () => {
const stores = fixtureStores()
stores.profileRecencyDb.put(NOPOST, { addr: NOPOST, blockHeight: 600900, seen: 9 })
await backfillProfileRecency(stores)
assert.isFalse(stores.profileRecencyDb.store.has(NOPOST))
})
it('should rethrow an unexpected store error instead of treating it as not found', async () => {
const stores = fixtureStores()
stores.profilesDb.get = async () => { throw new Error('store boom') }
let error
try {
await backfillProfileRecency(stores)
} catch (err) {
error = err
}
assert.equal(error?.message, 'store boom')
})
it('should treat every entry as confirmed when the status store has no chain tip', async () => {
const stores = fixtureStores()
stores.statusDb = new FakeDb()
await backfillProfileRecency(stores)
// With no tip, Bob's unconfirmed post-b2 at 600500 is the newest.
assert.deepEqual(stores.profileRecencyDb.store.get(BOB), {
addr: BOB, blockHeight: 600500, seen: 250
})
})
it('should treat a post exactly at the chain tip as confirmed', async () => {
const stores = fixtureStores()
stores.statusDb = new FakeDb([['status', { chainBlockHeight: 600500 }]])
await backfillProfileRecency(stores)
assert.deepEqual(stores.profileRecencyDb.store.get(BOB), {
addr: BOB, blockHeight: 600500, seen: 250
})
})
it('should not record a qualifying post from an address without a profile', async () => {
const stores = fixtureStores()
const noprofile = 'bitcoincash:qaddr-noprofile'
stores.addrPostHeightsDb.put(addrPostHeightKey(noprofile, 600100, 'post-np'), {
txid: 'post-np', addr: noprofile, blockHeight: 600100
})
stores.postsDb.put('post-np', { addr: noprofile, seen: 50, blockHeight: 600100 })
await backfillProfileRecency(stores)
assert.isFalse(stores.profileRecencyDb.store.has(noprofile))
})
it('should ignore an addrPostHeights entry with no txid', async () => {
const stores = fixtureStores()
stores.addrPostHeightsDb.put(addrPostHeightKey(ALICE, 600350, ''), null)
await backfillProfileRecency(stores)
// The malformed entry must not displace Alice's real post-a1.
assert.deepEqual(stores.profileRecencyDb.store.get(ALICE), {
addr: ALICE, blockHeight: 600100, seen: 100
})
})
it('should default the seen time to 0 when the post record is missing', async () => {
const stores = fixtureStores()
const ghost = 'bitcoincash:qaddr-ghost'
stores.profilesDb.put(ghost, { text: 'ghost bio', txid: 'profile-ghost' })
stores.addrPostHeightsDb.put(addrPostHeightKey(ghost, 600100, 'post-ghost'), {
txid: 'post-ghost', addr: ghost, blockHeight: 600100
})
await backfillProfileRecency(stores)
assert.deepEqual(stores.profileRecencyDb.store.get(ghost), {
addr: ghost, blockHeight: 600100, seen: 0
})
})
it('should tolerate missing optional reply/poll stores', async () => {
const stores = fixtureStores()
delete stores.postParentsDb
delete stores.pollsDb
await backfillProfileRecency(stores)
// Without the reply store, reply-a1 at 600300 becomes Alice's newest.
assert.deepEqual(stores.profileRecencyDb.store.get(ALICE), {
addr: ALICE, blockHeight: 600300, seen: 150
})
})
it('should pick the greater seen time for two posts at equal height', async () => {
const stores = fixtureStores()
const tie = 'bitcoincash:qaddr-tie'
stores.profilesDb.put(tie, { text: 'tie bio', txid: 'profile-tie' })
stores.addrPostHeightsDb.put(addrPostHeightKey(tie, 600100, 'post-t1'), {
txid: 'post-t1', addr: tie, blockHeight: 600100
})
stores.addrPostHeightsDb.put(addrPostHeightKey(tie, 600100, 'post-t2'), {
txid: 'post-t2', addr: tie, blockHeight: 600100
})
stores.postsDb.put('post-t1', { addr: tie, seen: 50, blockHeight: 600100 })
stores.postsDb.put('post-t2', { addr: tie, seen: 80, blockHeight: 600100 })
await backfillProfileRecency(stores)
assert.deepEqual(stores.profileRecencyDb.store.get(tie), {
addr: tie, blockHeight: 600100, seen: 80
})
})
})
describe('#partsFromAddrPostHeightKey', () => {
it('should recover the address, height, and txid from a key', () => {
assert.deepEqual(partsFromAddrPostHeightKey(addrPostHeightKey(ALICE, 600100, 'post-a1')), {
addr: ALICE,
blockHeight: 600100,
txid: 'post-a1'
})
})
it('should default a non-numeric height segment to 0', () => {
assert.deepEqual(partsFromAddrPostHeightKey(`${ALICE}::post-a1`), {
addr: ALICE,
blockHeight: 0,
txid: 'post-a1'
})
})
})