mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
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.
223 lines
7.6 KiB
JavaScript
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'
|
|
})
|
|
})
|
|
})
|