Refactor following feed cap: share acceptance seeding, add cap properties

Extract putPost/putFollowEdge seeding helpers in the psf-memo-db acceptance
handlers and route the following-feed capped/mixed fixtures and the
many-top-level-posts fixture through them, removing repeated store/key writes.

Add property tests for the capped total, newest-first page conservation, and
the bounded eligible scan, plus unit coverage for the remaining PostQuery
constructor guards and static key helpers (post-query.js statements 94.5% ->
99.4%).

CRAP stays at or below 6, src/adapters/post-query.js remains at 40 mutation
sites with its manifest intact, and the DRY tool reports no duplicates in the
changed source file.

By refactorer.
This commit is contained in:
Chris Troutner
2026-09-20 08:32:43 -07:00
parent db216949ec
commit 5653ecef9d
3 changed files with 196 additions and 38 deletions
+48 -38
View File
@@ -44,6 +44,39 @@ function padHeight (blockHeight) {
return String(blockHeight ?? 0).padStart(12, '0') return String(blockHeight ?? 0).padStart(12, '0')
} }
// Shared fixture seeding helpers so every loader writes the same post and
// follow records without repeating the store/key shape at each call site.
async function putFollowEdge (world, { followerAddr, followeeAddr, txid, blockHeight = 600000 }) {
const followeePkHash = hash160(followeeAddr)
await world.adapters.level.followsDb.put(`${followerAddr}:${followeePkHash}`, {
followerAddr,
followeePkHash,
unfollow: false,
txid,
seen: 1,
blockHeight
})
}
async function putPost (world, { txid, addr, text = txid, seen = 0, blockHeight, withAddrIndex = false }) {
await world.adapters.level.postsDb.put(txid, {
addr,
text,
seen,
blockHeight
})
await world.adapters.level.postHeightsDb.put(
`${padHeight(blockHeight)}:${txid}`,
{ txid, blockHeight }
)
if (withAddrIndex) {
await world.adapters.level.addrPostHeightsDb.put(
`${addr}:${padHeight(blockHeight)}:${txid}`,
{ txid, addr, blockHeight }
)
}
}
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
const tmpDir = path.resolve(__dirname, '..', '..', 'tmp', 'acceptance') const tmpDir = path.resolve(__dirname, '..', '..', 'tmp', 'acceptance')
@@ -467,22 +500,14 @@ async function loadManyTopLevelPosts (world) {
// index and reads all 510 entries. // index and reads all 510 entries.
for (let i = 0; i < 510; i++) { for (let i = 0; i < 510; i++) {
const id = String(i).padStart(3, '0') const id = String(i).padStart(3, '0')
const txid = `post-${id}` await putPost(world, {
const blockHeight = 600000 + i txid: `post-${id}`,
await world.adapters.level.postsDb.put(txid, {
addr: 'bitcoincash:qaddr', addr: 'bitcoincash:qaddr',
text: `post ${id}`, text: `post ${id}`,
seen: i, seen: i,
blockHeight blockHeight: 600000 + i,
withAddrIndex: true
}) })
await world.adapters.level.postHeightsDb.put(
String(blockHeight).padStart(12, '0') + ':' + txid,
{ txid, blockHeight }
)
await world.adapters.level.addrPostHeightsDb.put(
`bitcoincash:qaddr:${String(blockHeight).padStart(12, '0')}:${txid}`,
{ txid, addr: 'bitcoincash:qaddr', blockHeight }
)
} }
} }
@@ -491,30 +516,22 @@ async function loadFollowingFeedCapped (world) {
const followee = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' const followee = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
world.fixtureViewer = viewer world.fixtureViewer = viewer
await world.adapters.level.followsDb.put(`${viewer}:${hash160(followee)}`, { await putFollowEdge(world, {
followerAddr: viewer, followerAddr: viewer,
followeePkHash: hash160(followee), followeeAddr: followee,
unfollow: false, txid: 'follow-capped'
txid: 'follow-capped',
seen: 1,
blockHeight: 600000
}) })
// 510 eligible top-level posts, larger than the 500 total-scan cap. // 510 eligible top-level posts, larger than the 500 total-scan cap.
for (let i = 0; i < 510; i++) { for (let i = 0; i < 510; i++) {
const id = String(i).padStart(3, '0') const id = String(i).padStart(3, '0')
const txid = `post-${id}` await putPost(world, {
const blockHeight = 600000 + i txid: `post-${id}`,
await world.adapters.level.postsDb.put(txid, {
addr: followee, addr: followee,
text: `post ${id}`, text: `post ${id}`,
seen: i, seen: i,
blockHeight blockHeight: 600000 + i
}) })
await world.adapters.level.postHeightsDb.put(
`${padHeight(blockHeight)}:${txid}`,
{ txid, blockHeight }
)
} }
} }
@@ -527,13 +544,10 @@ async function loadFollowingFeedMixed (world) {
const other = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a' const other = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a'
world.fixtureViewer = viewer world.fixtureViewer = viewer
await world.adapters.level.followsDb.put(`${viewer}:${hash160(followee)}`, { await putFollowEdge(world, {
followerAddr: viewer, followerAddr: viewer,
followeePkHash: hash160(followee), followeeAddr: followee,
unfollow: false, txid: 'follow-mixed'
txid: 'follow-mixed',
seen: 1,
blockHeight: 600000
}) })
const posts = [ const posts = [
@@ -544,16 +558,12 @@ async function loadFollowingFeedMixed (world) {
{ txid: 'reply-A2', addr: followee, blockHeight: 600300 } { txid: 'reply-A2', addr: followee, blockHeight: 600300 }
] ]
for (const post of posts) { for (const post of posts) {
await world.adapters.level.postsDb.put(post.txid, { await putPost(world, {
txid: post.txid,
addr: post.addr, addr: post.addr,
text: post.txid,
seen: post.blockHeight, seen: post.blockHeight,
blockHeight: post.blockHeight blockHeight: post.blockHeight
}) })
await world.adapters.level.postHeightsDb.put(
`${padHeight(post.blockHeight)}:${post.txid}`,
{ txid: post.txid, blockHeight: post.blockHeight }
)
} }
const reply = { txid: 'reply-A2', parentTxid: 'post-A2', childTxid: 'reply-A2', blockHeight: 600300 } const reply = { txid: 'reply-A2', parentTxid: 'post-A2', childTxid: 'reply-A2', blockHeight: 600300 }
@@ -185,3 +185,98 @@ test('following-feed scan never returns replies, the viewer, or un-followed auth
{ label: 'following-feed reply/viewer/membership exclusion' } { label: 'following-feed reply/viewer/membership exclusion' }
) )
}) })
// All-eligible corpus: every indexed entry is a top-level post by the single
// followed author, so the scan's eligible count equals the number of posts it
// reads. `cap` varies the total-scan cap so the capped and below-cap branches
// are both exercised.
function cappedCorpusGen () {
return () => {
const n = intGen(rng, 0, 25)()
const followee = 'bitcoincash:f1'
const posts = []
const postHeights = []
for (let i = 0; i < n; i++) {
const txid = txidGen(rng)
const height = intGen(rng, 0, 9000000)()
posts.push({ txid, addr: followee, text: 'post ' + i, seen: i, blockHeight: height })
postHeights.push({ key: PostQuery.postHeightKey(height, txid), value: { txid } })
}
return {
n,
posts,
postHeights,
followed: [VIEWER, followee],
limit: intGen(rng, 1, 8)(),
offset: intGen(rng, 0, 12)(),
cap: intGen(rng, 1, 12)()
}
}
}
function orderedPostTxids (posts) {
return [...posts]
.sort((a, b) => (PostQuery.postHeightKey(a.blockHeight, a.txid) < PostQuery.postHeightKey(b.blockHeight, b.txid) ? 1 : -1))
.map((p) => p.txid)
}
test('following-feed scan caps the total and conserves the newest-first page', async () => {
await forAll(
cappedCorpusGen(),
async ({ n, posts, postHeights, followed, limit, offset, cap }) => {
const query = makeQuery(postHeights, posts, new Set())
const { txids, total } = await query.scanFollowingFeedTxidsAndCount(
VIEWER,
followed,
{ limit, offset, totalScanCap: cap }
)
const expectedTotal = Math.min(n, cap)
const expectedTxids = orderedPostTxids(posts).slice(offset, offset + limit)
return total === expectedTotal && JSON.stringify(txids) === JSON.stringify(expectedTxids)
},
{ label: 'following-feed capped-total and page conservation' }
)
})
test('following-feed scan reads at most offset + limit + cap eligible posts', async () => {
await forAll(
cappedCorpusGen(),
async ({ n, posts, postHeights, followed, limit, offset, cap }) => {
const counter = { reads: 0 }
const store = new Map(posts.map((p) => [p.txid, p]))
const query = new PostQuery({
postsDb: {
async get (txid) {
counter.reads++
const post = store.get(txid)
if (!post) {
const err = new Error('not found')
err.notFound = true
throw err
}
return post
}
},
postHeightsDb: makePostHeightsDb(postHeights),
addrPostHeightsDb: {},
postParentsDb: makeParentsDb(new Set()),
postChildrenDb: {},
likesDb: {},
postLikesDb: {}
})
await query.scanFollowingFeedTxidsAndCount(
VIEWER,
followed,
{ limit, offset, totalScanCap: cap }
)
return counter.reads === Math.min(n, offset + limit + cap)
},
{ label: 'following-feed bounded eligible scan' }
)
})
@@ -94,6 +94,59 @@ describe('#PostQuery', () => {
} }
}) })
it('should throw when postsDb is missing', () => {
try {
// eslint-disable-next-line no-new
new PostQuery({ postHeightsDb, addrPostHeightsDb, postParentsDb, postChildrenDb, likesDb, postLikesDb })
assert.fail('Expected error')
} catch (err) {
assert.include(err.message, 'postsDb required')
}
})
it('should throw when postParentsDb is missing', () => {
try {
// eslint-disable-next-line no-new
new PostQuery({ postsDb, postHeightsDb, addrPostHeightsDb, postChildrenDb, likesDb, postLikesDb })
assert.fail('Expected error')
} catch (err) {
assert.include(err.message, 'postParentsDb required')
}
})
it('should throw when postChildrenDb is missing', () => {
try {
// eslint-disable-next-line no-new
new PostQuery({ postsDb, postHeightsDb, addrPostHeightsDb, postParentsDb, likesDb, postLikesDb })
assert.fail('Expected error')
} catch (err) {
assert.include(err.message, 'postChildrenDb required')
}
})
it('should throw when likesDb is missing', () => {
try {
// eslint-disable-next-line no-new
new PostQuery({ postsDb, postHeightsDb, addrPostHeightsDb, postParentsDb, postChildrenDb, postLikesDb })
assert.fail('Expected error')
} catch (err) {
assert.include(err.message, 'likesDb required')
}
})
describe('#static key helpers', () => {
it('should pad block heights to a fixed width for lexicographic ordering', () => {
assert.equal(PostQuery.padHeight(600200), '000000600200')
assert.equal(PostQuery.padHeight(1), '000000000001')
})
it('should compose postHeight, addrPostHeight, and postLike keys', () => {
assert.equal(PostQuery.postHeightKey(600200, 'post-a'), '000000600200:post-a')
assert.equal(PostQuery.addrPostHeightKey('bitcoincash:addr', 600200, 'post-a'), 'bitcoincash:addr:000000600200:post-a')
assert.equal(PostQuery.postLikeKey('post-a', 'like-1'), 'post-a:like-1')
})
})
describe('#txidFromPostHeight', () => { describe('#txidFromPostHeight', () => {
it('should return the txid from the value when present', () => { it('should return the txid from the value when present', () => {
assert.equal(uut.txidFromPostHeight('any-key', { txid: 'abc123' }), 'abc123') assert.equal(uut.txidFromPostHeight('any-key', { txid: 'abc123' }), 'abc123')