Harden and review like-count-display refactor

- Add unit tests to kill all mutation survivors in GetPostThread
  (sort order, missing blockHeight/seen fallbacks, non-string txid).
- Add default-limit and falsy-pagination tests for the client page
  controllers (ProfilePage, RecentFeedPage).
- Refresh mutation manifests and Gherkin acceptance mutation metadata.
- Write durable review report to docs/reviews/like-count-display-summary.md.

By architect.
This commit is contained in:
Chris Troutner
2026-08-26 20:46:35 -07:00
parent c208eb38aa
commit 21373f4d10
13 changed files with 198 additions and 4 deletions
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-08-27T03:45:29.024609067Z","feature_name":"Like Count Display","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/like-count-display.feature","background_hash":"fbe6cdb3df821009903f22248beed2018e00df162f6df280b81686214168733f","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Like Count Display - 1 the recent feed shows the like count returned by the API for each post","scenario_hash":"812ee12d4f7968f5c7024a15297854aa8c900ae7b19972718875e77c67106a65","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-27T03:40:43.303265742Z"},{"index":2,"name":"Like Count Display - 3 the profile page shows the like count returned by the API for each post","scenario_hash":"cdc53b5eddbc3f103472495a7d6650def4cd586c1567aab01f55fdc4989a5a8c","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-27T03:40:43.303265742Z"}]}
# acceptance-mutation-manifest-end
# Scenarios: Like Count Display - 1, Like Count Display - 2, Like Count Display - 3, Like Count Display - 4
Feature: Like Count Display
@@ -43,3 +43,7 @@ class ProfilePage {
ProfilePage.PROFILE_PATH_PREFIX = PROFILE_PATH_PREFIX
module.exports = ProfilePage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T03:38:35.169Z","module_hash":"3d7b866f2997caa538addbc83086aec9b697f318a83cdc976d7f976cefa38e99","functions":[{"id":"func/ProfilePage.constructor","name":"ProfilePage.constructor","line":16,"end_line":21,"hash":"7c71c8c700d9f388b7a6904b8b3fae0f4c50e23288e96ac90e26f89797c16136"},{"id":"func/ProfilePage.load","name":"ProfilePage.load","line":23,"end_line":36,"hash":"6ccf9d1559cc12ba56d876cf68c2b0f369f195ec2d1058451c0b3bb9dcd3a515"},{"id":"func/ProfilePage.getPost","name":"ProfilePage.getPost","line":38,"end_line":40,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]}
// mutate4javascript-manifest-end
@@ -39,3 +39,7 @@ class RecentFeedPage {
RecentFeedPage.RECENT_FEED_PATH = RECENT_FEED_PATH
module.exports = RecentFeedPage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T03:38:52.144Z","module_hash":"e2ee3beff9079c7f93d29043b1428c271fbd1a2b64f2635fbc2cc06fb52a3aab","functions":[{"id":"func/RecentFeedPage.constructor","name":"RecentFeedPage.constructor","line":16,"end_line":20,"hash":"0eb6faf1dfd73ad95b210473ab82fdae513ff1570e401820645661881a2f8d4b"},{"id":"func/RecentFeedPage.load","name":"RecentFeedPage.load","line":22,"end_line":32,"hash":"96e8f2c77aadf98e3a9e71e1f0f6ce98a3e47dd828ffb4626916f56ca32ed137"},{"id":"func/RecentFeedPage.getPost","name":"RecentFeedPage.getPost","line":34,"end_line":36,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]}
// mutate4javascript-manifest-end
@@ -49,3 +49,7 @@ class ThreadPage {
ThreadPage.THREAD_PATH_PREFIX = THREAD_PATH_PREFIX
module.exports = ThreadPage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T03:37:28.366Z","module_hash":"72c6d473102b96ef23e3febd49bde31801b4c847ec1d08aeeff26e6efbae025a","functions":[{"id":"func/ThreadPage.constructor","name":"ThreadPage.constructor","line":16,"end_line":20,"hash":"991bc60db7889f60ab1d0b129c8f588ce964ea3dc8e71d44abb8bdc8452dd0b7"},{"id":"func/ThreadPage.load","name":"ThreadPage.load","line":22,"end_line":35,"hash":"4ee1a6881f170ef49c104e4135b41fbfd381de913d801e48d4d3ea2df1fd6113"},{"id":"func/ThreadPage.getPost","name":"ThreadPage.getPost","line":37,"end_line":39,"hash":"316894eedc9fc002bae913451ba8c304bc587dca44c1769b3af377a9084c5e28"},{"id":"func/ThreadPage._flatten","name":"ThreadPage._flatten","line":41,"end_line":46,"hash":"e4cedb80dd8392fd846f575112162f9b5ca3d6ee6749f775704b48e265045b8a"}]}
// mutate4javascript-manifest-end
@@ -47,3 +47,34 @@ test('load throws when no memo db client is provided', async () => {
/requires a memo db client/
)
})
test('load defaults limit to 100 and offset to 0', async () => {
const calls = []
const addr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
const memoDb = {
async getPostsByAddr (a, params) {
calls.push({ a, params })
return { posts: [], pagination: {} }
}
}
const page = new ProfilePage({ memoDb, addr })
await page.load()
assert.deepEqual(calls, [{ a: addr, params: { limit: 100, offset: 0 } }])
})
test('load sets pagination to null when the API returns none', async () => {
const addr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
const memoDb = {
async getPostsByAddr () {
return { posts: [], pagination: undefined }
}
}
const page = new ProfilePage({ memoDb, addr })
const result = await page.load()
assert.equal(result.pagination, null)
assert.equal(page.pagination, null)
})
@@ -65,3 +65,18 @@ test('load throws when no memo db client is provided', async () => {
/requires a memo db client/
)
})
test('load defaults limit to 100 and offset to 0', async () => {
const calls = []
const memoDb = {
async getRecentPosts (params) {
calls.push(params)
return { posts: [], pagination: {} }
}
}
const page = new RecentFeedPage({ memoDb })
await page.load()
assert.deepEqual(calls, [{ limit: 100, offset: 0 }])
})
+5
View File
@@ -1,3 +1,8 @@
# mutation-stamp: sha256=9685d4d281c33b449530a853441ee46b2b27b8907e09bb460780c6f463f45075
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-08-27T03:40:40.281402104Z","feature_name":"Like counts on posts","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/like-counts.feature","background_hash":"30a5bc9082cb9530e629dfeef1897d58e0fbd06c2e6e6e9824cfd8d29a821bfc","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Like counts on posts - 1 GET /posts/recent includes likeCount for each top-level post","scenario_hash":"93f4b6a672ca408ba714219ee2ed3090ad7dcdcded70d3910929936d62b161ad","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-08-27T03:40:40.281402104Z"},{"index":1,"name":"Like counts on posts - 2 GET /posts/by/:addr includes likeCount for that address's posts","scenario_hash":"c4aeb8aec676bc5e6d622646788838e4131d4c7f5852e585885757c1de9b5d5b","mutation_count":9,"result":{"Total":9,"Killed":9,"Survived":0,"Errors":0},"tested_at":"2026-08-27T03:40:40.281402104Z"},{"index":2,"name":"Like counts on posts - 3 GET /posts/:txid/thread includes likeCount on the root and replies","scenario_hash":"87302ac20d7b363f59683f1f334f6a1000242d76bb0ba844578f4b2c8a0f2b9c","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-08-27T03:40:40.281402104Z"},{"index":3,"name":"Like counts on posts - 4 likes for posts that are not in the posts store are ignored","scenario_hash":"00f89904d11df1636df9913b9212c9cada3d20955d6c60ddf8af793988b16e92","mutation_count":2,"result":{"Total":2,"Killed":2,"Survived":0,"Errors":0},"tested_at":"2026-08-27T03:40:40.281402104Z"}]}
# acceptance-mutation-manifest-end
# Scenarios: Like counts on posts - 1, Like counts on posts - 2, Like counts on posts - 3, Like counts on posts - 4
Feature: Like counts on posts
+1 -1
View File
@@ -196,5 +196,5 @@ class PostQuery {
export default PostQuery
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-26T18:11:36.339Z","module_hash":"9d849ae3741dcb65f12ea75f911bf873e36f5748183df777be6296c709c5e598","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":8,"end_line":37,"hash":"75efd21f33ee6a78e6c41e71b2b7dea75a7b66bb2f7e27569864d0c9a4843383"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":39,"end_line":41,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":43,"end_line":45,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":47,"end_line":51,"hash":"691bcf6f1ab70e0608e3f05da9b9f7d88cc8ac710cdcb14e6dc4a1c1c0546744"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":53,"end_line":61,"hash":"a397af5257d234a2aa9c18b1de49aa3738bf31645e0efbb9ed71bd0940abdb18"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":63,"end_line":73,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":76,"end_line":83,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":87,"end_line":95,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":97,"end_line":112,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":114,"end_line":132,"hash":"6485e255e529be0172debb67749fc2fc1757e5c41f6d03fd14f8d277aea4b91a"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":134,"end_line":150,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":152,"end_line":163,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":165,"end_line":174,"hash":"d5bcd0c7140f6c05cc3e66037ca96627ff262dd975a5553483bbd8f91bfdd7f5"}]}
// {"version":1,"tested_at":"2026-08-27T03:28:06.470Z","module_hash":"62649e78ba195018e963925a0e47cb5915bba05499c3b04553eecceb37ef813a","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":8,"end_line":42,"hash":"3e32c5868d6bc9ea39cc8d7e87f646a2dc7d03663d7cd54029200b754afe27b7"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":44,"end_line":46,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":48,"end_line":50,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":52,"end_line":56,"hash":"691bcf6f1ab70e0608e3f05da9b9f7d88cc8ac710cdcb14e6dc4a1c1c0546744"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":58,"end_line":66,"hash":"a397af5257d234a2aa9c18b1de49aa3738bf31645e0efbb9ed71bd0940abdb18"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":68,"end_line":78,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":80,"end_line":92,"hash":"ff88d216af5734cd7b1eab31b87e0b412c2cac15eb7e6962465827a0ecaf3e4d"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":95,"end_line":102,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":106,"end_line":114,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":116,"end_line":131,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":133,"end_line":151,"hash":"6485e255e529be0172debb67749fc2fc1757e5c41f6d03fd14f8d277aea4b91a"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":153,"end_line":169,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":171,"end_line":182,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":184,"end_line":193,"hash":"d5bcd0c7140f6c05cc3e66037ca96627ff262dd975a5553483bbd8f91bfdd7f5"}]}
// mutate4javascript-manifest-end
@@ -128,3 +128,7 @@ class GetPostThread {
}
export default GetPostThread
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T03:24:13.418Z","module_hash":"3270d9244ee4517c8d64770645d4b38e8d1937a4bb13beea79d0611960305aea","functions":[{"id":"func/GetPostThread.constructor","name":"GetPostThread.constructor","line":6,"end_line":21,"hash":"b23e82ab887fb68d199f23b4c6d94f6417556cbf7f7eeaeff7efdb5cce74f3f0"},{"id":"func/GetPostThread.execute","name":"GetPostThread.execute","line":23,"end_line":46,"hash":"968030072d4946cd4d00832fb334dc25d1286b24c1bc71252d30de6e3faebd38"},{"id":"func/GetPostThread.attachLikeCounts","name":"GetPostThread.attachLikeCounts","line":48,"end_line":55,"hash":"f7ba4717fd0cd0ba0c6cd3f7571c071a00bfc666076023a323d4848d6d4ba720"},{"id":"func/GetPostThread.fetchPostOrNull","name":"GetPostThread.fetchPostOrNull","line":57,"end_line":67,"hash":"b853fa9e3b121a3a0fae8c6828865039add3ec5950ae4cfa2db252f9db09446c"},{"id":"func/GetPostThread.loadChildTxids","name":"GetPostThread.loadChildTxids","line":69,"end_line":83,"hash":"9c1a7feff0dd92624b77127de70b2b13eea13854cf2e807b9c2c635b072a932f"},{"id":"func/GetPostThread.compareReplies","name":"GetPostThread.compareReplies","line":85,"end_line":94,"hash":"60393ba5137feda1df76d54f2b5c4da0728cc0dd556d72748ff3ba69328ba9a9"},{"id":"func/GetPostThread.buildThreadNode","name":"GetPostThread.buildThreadNode","line":96,"end_line":127,"hash":"e5ca3213bad9d22d9a87a2cf24f55cc5ffc4a6acd2d0e6c026ade991b490f43f"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -72,5 +72,5 @@ export function assemblePostPage ({ posts, replyCounts, likeCounts, total, limit
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-26T18:14:53.748Z","module_hash":"b9ba853191484c924ee7003cee18435e2aa00057155d94e573939f2856cf006c","functions":[{"id":"func/isEmpty","name":"isEmpty","line":13,"end_line":15,"hash":"209ecce14d6de3b7500065dd3091a7f2c006d1b08305078720ddd2ab250bc501"},{"id":"func/httpError","name":"httpError","line":17,"end_line":21,"hash":"270f69e0007e397fbd6beac1f2fd7d3a1156f9ba1cdf2d230e4e867c8b221aeb"},{"id":"func/parseLimit","name":"parseLimit","line":23,"end_line":34,"hash":"20c64d382dedbd2b65e68d66a35ccba8ecfc78dc32534a697e3865f05021eaf2"},{"id":"func/parseOffset","name":"parseOffset","line":36,"end_line":44,"hash":"55b13c976e2d53e6b69658555d97f4e63d425b5cb6b7b47b57081bd18ef16845"},{"id":"func/attachReplyCounts","name":"attachReplyCounts","line":46,"end_line":51,"hash":"99b9e818ac032752eb76fc9fcdcba0dcd1d94bee830921131a18fd01a4848f76"},{"id":"func/assemblePostPage","name":"assemblePostPage","line":53,"end_line":63,"hash":"c605049c633f08e0bd8247d6147b4a2a0ba6652f258f3f44852775f08d6c1e35"}]}
// {"version":1,"tested_at":"2026-08-27T03:32:37.566Z","module_hash":"e13c6a9aac8093c65559ec81bc44239611f47270c9199a8b4dd8fc67b36a0e5f","functions":[{"id":"func/isEmpty","name":"isEmpty","line":13,"end_line":15,"hash":"209ecce14d6de3b7500065dd3091a7f2c006d1b08305078720ddd2ab250bc501"},{"id":"func/httpError","name":"httpError","line":17,"end_line":21,"hash":"270f69e0007e397fbd6beac1f2fd7d3a1156f9ba1cdf2d230e4e867c8b221aeb"},{"id":"func/parseLimit","name":"parseLimit","line":23,"end_line":34,"hash":"20c64d382dedbd2b65e68d66a35ccba8ecfc78dc32534a697e3865f05021eaf2"},{"id":"func/parseOffset","name":"parseOffset","line":36,"end_line":44,"hash":"55b13c976e2d53e6b69658555d97f4e63d425b5cb6b7b47b57081bd18ef16845"},{"id":"func/attachReplyCounts","name":"attachReplyCounts","line":46,"end_line":51,"hash":"99b9e818ac032752eb76fc9fcdcba0dcd1d94bee830921131a18fd01a4848f76"},{"id":"func/attachLikeCounts","name":"attachLikeCounts","line":53,"end_line":58,"hash":"eebdaa4235d209f5ab3cfb7c242bb76b29c0365676107eee7f681ecb1c188ea8"},{"id":"func/assemblePostPage","name":"assemblePostPage","line":60,"end_line":72,"hash":"496cbbf9f16d3b36ffbe486a62462cbb939f02b9f1610e4a577d2e5ff167c51f"}]}
// mutate4javascript-manifest-end
@@ -40,5 +40,5 @@ class ListPostsByAddr extends ListUseCase {
export default ListPostsByAddr
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-26T18:15:16.107Z","module_hash":"b925b4bf1307be2f9d7b4e5d9590f6d189c5ec7632102bb65cdb525ffe19f7b3","functions":[{"id":"func/ListPostsByAddr.constructor","name":"ListPostsByAddr.constructor","line":10,"end_line":12,"hash":"f777f3685c5b2199ec3c3a7043b3cf288a9bbf583232bd1fc58cc346373f03d6"},{"id":"func/ListPostsByAddr.parseAddr","name":"ListPostsByAddr.parseAddr","line":14,"end_line":21,"hash":"0f02fc6ebf05826429d57bf2fd64bf66dfde96ee159f6c1d4814f41cc7a25aca"},{"id":"func/ListPostsByAddr.execute","name":"ListPostsByAddr.execute","line":23,"end_line":36,"hash":"899d7d2e5c9c31a05c34f00c063ffd4f88a6d0a779f6d47000f2fb770029dc86"}]}
// {"version":1,"tested_at":"2026-08-27T03:36:35.828Z","module_hash":"fe2572105f71125c76e822f2fe6ab15b603ff571753d6e1f07870c1e7b4f4a95","functions":[{"id":"func/ListPostsByAddr.constructor","name":"ListPostsByAddr.constructor","line":10,"end_line":12,"hash":"f777f3685c5b2199ec3c3a7043b3cf288a9bbf583232bd1fc58cc346373f03d6"},{"id":"func/ListPostsByAddr.parseAddr","name":"ListPostsByAddr.parseAddr","line":14,"end_line":21,"hash":"0f02fc6ebf05826429d57bf2fd64bf66dfde96ee159f6c1d4814f41cc7a25aca"},{"id":"func/ListPostsByAddr.execute","name":"ListPostsByAddr.execute","line":23,"end_line":37,"hash":"f792df1eb2aa025d2f9ad2f88ee0034c37e0502e035afc3430d448cb6b97b5d7"}]}
// mutate4javascript-manifest-end
@@ -30,5 +30,5 @@ class ListRecentPosts extends ListUseCase {
export default ListRecentPosts
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-26T18:15:24.894Z","module_hash":"35af93129e7bf3eddc46fa5909fdf71538afc29182af5a1a2ec06e25193fa7d3","functions":[{"id":"func/ListRecentPosts.constructor","name":"ListRecentPosts.constructor","line":10,"end_line":12,"hash":"0abcf69664af3b707dfe95a9db5caa65e3bbec6dfb740393cafb923e81aad9ae"},{"id":"func/ListRecentPosts.execute","name":"ListRecentPosts.execute","line":14,"end_line":26,"hash":"ff4bd895e3dad7ec3ed58832588e53f049de4f210a19f81ef3e074abf694dd33"}]}
// {"version":1,"tested_at":"2026-08-27T03:37:04.275Z","module_hash":"32bded9680fed44f6f0f8ecd55ff1533ed80ef30a72001a26c562d31fa39dbc2","functions":[{"id":"func/ListRecentPosts.constructor","name":"ListRecentPosts.constructor","line":10,"end_line":12,"hash":"0abcf69664af3b707dfe95a9db5caa65e3bbec6dfb740393cafb923e81aad9ae"},{"id":"func/ListRecentPosts.execute","name":"ListRecentPosts.execute","line":14,"end_line":27,"hash":"ea22fb6f9232a570503419bff406f4d87b8a3f9debe1d4020c0c600a13dfd29d"}]}
// mutate4javascript-manifest-end
@@ -15,6 +15,11 @@ describe('#GetPostThread', () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
Object.assign(mockPosts, {
'root-1': { addr: 'addr-a', text: 'root', seen: 100, blockHeight: 600100 },
'reply-1': { addr: 'addr-a', text: 'reply', seen: 90, blockHeight: 600090 },
'reply-2': { addr: 'addr-b', text: 'reply b', seen: 80, blockHeight: 600080 }
})
postQuery = {
postsDb: {
get: sandbox.stub().callsFake(async (txid) => {
@@ -50,6 +55,16 @@ describe('#GetPostThread', () => {
}
})
it('should reject a non-string txid', async () => {
try {
await uut.execute({ txid: 12345 })
assert.fail('Expected error')
} catch (err) {
assert.equal(err.status, 400)
assert.include(err.message, 'transaction ID is required')
}
})
it('should return 404 when the post is missing', async () => {
try {
await uut.execute({ txid: 'missing' })
@@ -72,4 +87,112 @@ describe('#GetPostThread', () => {
assert.equal(reply1.likeCount, 2)
assert.equal(reply2.likeCount, 0)
})
it('should sort replies by blockHeight ascending then seen ascending', async () => {
const result = await uut.execute({ txid: 'root-1' })
const txids = result.post.replies.map((r) => r.txid)
// reply-2 has blockHeight 600080, reply-1 has 600090.
assert.deepEqual(txids, ['reply-2', 'reply-1'])
})
it('should tie-break replies with equal blockHeight by seen ascending', async () => {
postQuery.postChildrenDb.iterator = sandbox.stub().callsFake(function * () {
yield ['root-1:reply-1', { parentTxid: 'root-1', childTxid: 'reply-1' }]
yield ['root-1:reply-2', { parentTxid: 'root-1', childTxid: 'reply-2' }]
})
mockPosts['reply-1'] = { addr: 'addr-a', text: 'reply', seen: 90, blockHeight: 600100 }
mockPosts['reply-2'] = { addr: 'addr-b', text: 'reply b', seen: 80, blockHeight: 600100 }
const result = await uut.execute({ txid: 'root-1' })
const txids = result.post.replies.map((r) => r.txid)
// Equal blockHeight: reply-2 (seen 80) sorts before reply-1 (seen 90).
assert.deepEqual(txids, ['reply-2', 'reply-1'])
})
it('should default missing blockHeight to 0 in the thread node', async () => {
mockPosts['root-1'] = { addr: 'addr-a', text: 'root', seen: 100 }
mockPosts['reply-1'] = { addr: 'addr-a', text: 'reply', seen: 90 }
mockPosts['reply-2'] = { addr: 'addr-b', text: 'reply b', seen: 80 }
const result = await uut.execute({ txid: 'root-1' })
assert.equal(result.post.blockHeight, 0)
for (const reply of result.post.replies) {
assert.equal(reply.blockHeight, 0)
}
})
it('should default missing seen to 0 when comparing replies', async () => {
postQuery.postChildrenDb.iterator = sandbox.stub().callsFake(function * () {
yield ['root-1:reply-1', { parentTxid: 'root-1', childTxid: 'reply-1' }]
yield ['root-1:reply-2', { parentTxid: 'root-1', childTxid: 'reply-2' }]
})
mockPosts['reply-1'] = { addr: 'addr-a', text: 'reply', blockHeight: 600100 }
mockPosts['reply-2'] = { addr: 'addr-b', text: 'reply b', seen: 0, blockHeight: 600100 }
const result = await uut.execute({ txid: 'root-1' })
const txids = result.post.replies.map((r) => r.txid)
// Equal blockHeight: reply-1 (missing seen => 0) ties with reply-2 (seen 0)
// and keeps insertion order, so reply-1 sorts first.
assert.deepEqual(txids, ['reply-1', 'reply-2'])
})
it('should default missing blockHeight to 0 when comparing replies', async () => {
postQuery.postChildrenDb.iterator = sandbox.stub().callsFake(function * () {
yield ['root-1:reply-1', { parentTxid: 'root-1', childTxid: 'reply-1' }]
yield ['root-1:reply-2', { parentTxid: 'root-1', childTxid: 'reply-2' }]
})
mockPosts['reply-1'] = { addr: 'addr-a', text: 'reply', seen: 100 }
mockPosts['reply-2'] = { addr: 'addr-b', text: 'reply b', seen: 100, blockHeight: 0 }
const result = await uut.execute({ txid: 'root-1' })
const txids = result.post.replies.map((r) => r.txid)
// Equal seen: reply-1 (missing blockHeight => 0) ties with reply-2 (blockHeight 0)
// and keeps insertion order, so reply-1 sorts first.
assert.deepEqual(txids, ['reply-1', 'reply-2'])
})
it('should treat a missing blockHeight on the second operand as 0', async () => {
postQuery.postChildrenDb.iterator = sandbox.stub().callsFake(function * () {
yield ['root-1:reply-1', { parentTxid: 'root-1', childTxid: 'reply-1' }]
yield ['root-1:reply-2', { parentTxid: 'root-1', childTxid: 'reply-2' }]
})
mockPosts['reply-1'] = { addr: 'addr-a', text: 'reply', seen: 100, blockHeight: 1 }
mockPosts['reply-2'] = { addr: 'addr-b', text: 'reply b', seen: 100 }
const result = await uut.execute({ txid: 'root-1' })
const txids = result.post.replies.map((r) => r.txid)
// reply-1 blockHeight 1 > reply-2 missing (=> 0), so reply-1 sorts after reply-2.
assert.deepEqual(txids, ['reply-2', 'reply-1'])
})
it('should treat a missing seen on the second operand as 0', async () => {
postQuery.postChildrenDb.iterator = sandbox.stub().callsFake(function * () {
yield ['root-1:reply-1', { parentTxid: 'root-1', childTxid: 'reply-1' }]
yield ['root-1:reply-2', { parentTxid: 'root-1', childTxid: 'reply-2' }]
})
mockPosts['reply-1'] = { addr: 'addr-a', text: 'reply', seen: 1, blockHeight: 600100 }
mockPosts['reply-2'] = { addr: 'addr-b', text: 'reply b', blockHeight: 600100 }
const result = await uut.execute({ txid: 'root-1' })
const txids = result.post.replies.map((r) => r.txid)
// Equal blockHeight: reply-1 seen 1 > reply-2 missing (=> 0), so reply-1 sorts after reply-2.
assert.deepEqual(txids, ['reply-2', 'reply-1'])
})
it('compareReplies defaults a missing first blockHeight to 0', () => {
// a has no blockHeight (=> 0), b has blockHeight 1: a sorts before b.
assert.ok(uut.compareReplies({ seen: 0 }, { seen: 0, blockHeight: 1 }) < 0)
})
it('compareReplies defaults a missing second blockHeight to 0', () => {
// a has blockHeight 1, b has no blockHeight (=> 0): a sorts after b.
assert.ok(uut.compareReplies({ seen: 0, blockHeight: 1 }, { seen: 0 }) > 0)
})
})