mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Review topic-recency-pagination: harden mutation coverage
Architectural review of the topic recency indexes and pagination: - Indexer topic index maintenance lives in its own topic-indexing.js, separate from the action handlers; the backfill library takes injected LevelDB handles so its logic stays testable. - DB listTopics reads counts from topicSummaries and paginates the topicRecency index without iterating the rooms store; the client reuses PaginatedPage. - Kill four topic-indexing mutation survivors with focused unit tests covering the height fallbacks, plus exact-key/isNotFound and max-size boundary tests. - One topic-message survivor is an intrinsic equivalent: handlePost reads the text at push index 1 and ignores push index 0. Includes tool-written mutate4javascript and acceptance-mutation manifests. By architect.
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
# acceptance-mutation-manifest-begin
|
||||||
|
# {"version":1,"tested_at":"2026-09-17T17:25:15.930044025Z","feature_name":"Topic Pagination","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/topic-pagination.feature","background_hash":"74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Topic Pagination - 2 the topics page can load a later page","scenario_hash":"1d76d7c408593ae66f0f1551b1d583ccdceb0931d1dae35f5d02f8a0cb9636e9","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:25:15.930044025Z"}]}
|
||||||
|
# acceptance-mutation-manifest-end
|
||||||
|
|
||||||
# Scenarios: Topic Pagination - 1, Topic Pagination - 2
|
# Scenarios: Topic Pagination - 1, Topic Pagination - 2
|
||||||
#
|
#
|
||||||
# The topics page loads topics in pages of 50 and can move to a later page. The
|
# The topics page loads topics in pages of 50 and can move to a later page. The
|
||||||
|
|||||||
@@ -39,5 +39,5 @@ TopicDiscoveryPage.topicFeedPath = function (room) {
|
|||||||
module.exports = TopicDiscoveryPage
|
module.exports = TopicDiscoveryPage
|
||||||
|
|
||||||
// mutate4javascript-manifest-begin
|
// mutate4javascript-manifest-begin
|
||||||
// {"version":1,"tested_at":"2026-08-28T15:33:23.381Z","module_hash":"a238b89f4edc83d06d6a424a0f2e19307187410fe8328b5220289dab5bf18fe6","functions":[{"id":"func/TopicDiscoveryPage.constructor","name":"TopicDiscoveryPage.constructor","line":12,"end_line":16,"hash":"5e7790aac90f4000c71b5e8a4b4b15c6b2895d1315cc05741a4fa2577c9f1d61"},{"id":"func/TopicDiscoveryPage.load","name":"TopicDiscoveryPage.load","line":18,"end_line":27,"hash":"2ce74ae73560c443d862dc97c65adda83ca5cd2bcc4c0df8bb87889d99b60f1d"},{"id":"func/TopicDiscoveryPage.getTopic","name":"TopicDiscoveryPage.getTopic","line":29,"end_line":31,"hash":"7faf4b56db0bc89d32176a1741546958495ba10b5f313581f321de47e58ecb95"},{"id":"func/TopicDiscoveryPage.openTopic","name":"TopicDiscoveryPage.openTopic","line":33,"end_line":37,"hash":"ebd17b5f176c964308b69705b7a53af4c9c392e9856308b3095d69151ebde9aa"}]}
|
// {"version":1,"tested_at":"2026-09-17T16:44:01.309Z","module_hash":"5d9d81d1a2fa72e4365f6db50999dc28988b51dfe9c90662187ebc8dc634bba9","functions":[{"id":"func/TopicDiscoveryPage.constructor","name":"TopicDiscoveryPage.constructor","line":14,"end_line":21,"hash":"78cbee16b0d08e3f695dc65865466d12424e67d40f16683603585a48adfc2510"},{"id":"func/TopicDiscoveryPage.getTopic","name":"TopicDiscoveryPage.getTopic","line":23,"end_line":25,"hash":"7faf4b56db0bc89d32176a1741546958495ba10b5f313581f321de47e58ecb95"},{"id":"func/TopicDiscoveryPage.openTopic","name":"TopicDiscoveryPage.openTopic","line":27,"end_line":31,"hash":"ebd17b5f176c964308b69705b7a53af4c9c392e9856308b3095d69151ebde9aa"}]}
|
||||||
// mutate4javascript-manifest-end
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# mutation-stamp: sha256=62b1088abcd456b0067ce18fd78f32c715254e912046c44d8d67fdd54417147f
|
||||||
|
# acceptance-mutation-manifest-begin
|
||||||
|
# {"version":1,"tested_at":"2026-09-17T17:23:44.068113041Z","feature_name":"Backfill topic indexes","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/backfill-topic-indexes.feature","background_hash":"386b39215fd2ac58af1a095ce21b3e5a94c26a052be33ebb653986f56d1cdaf5","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Backfill topic indexes - 1 backfill summarizes every room","scenario_hash":"98e2a49f35bf2c57825af892d343b091df3b8ea9938d88877112a244195e2b6a","mutation_count":9,"result":{"Total":9,"Killed":9,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:23:44.068113041Z"},{"index":1,"name":"Backfill topic indexes - 2 backfill builds the recency index","scenario_hash":"6210a596d0011bd42ab984430a01e639932e19bb99dd05e6b79c0e3d8e50ccdc","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:23:44.068113041Z"},{"index":2,"name":"Backfill topic indexes - 3 backfill is idempotent","scenario_hash":"93b5d81880c0f9bf768aa363353607761c7cebd4d6b1c6cef52eae0f40fd3cc0","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:23:44.068113041Z"}]}
|
||||||
|
# acceptance-mutation-manifest-end
|
||||||
|
|
||||||
# Scenarios: Backfill topic indexes - 1, Backfill topic indexes - 2, Backfill topic indexes - 3
|
# Scenarios: Backfill topic indexes - 1, Backfill topic indexes - 2, Backfill topic indexes - 3
|
||||||
#
|
#
|
||||||
# The topic backfill utility builds topicSummaries and topicRecency from the
|
# The topic backfill utility builds topicSummaries and topicRecency from the
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# acceptance-mutation-manifest-begin
|
||||||
|
# {"version":1,"tested_at":"2026-09-17T17:22:41.386110241Z","feature_name":"Topic Pagination","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/topic-pagination.feature","background_hash":"4a01ae658dda9a91b7f19edbeb3e657bef1d666b5355b8dd30496013bed35d7e","implementation_hash":"unknown","scenarios":[]}
|
||||||
|
# acceptance-mutation-manifest-end
|
||||||
|
|
||||||
# Scenarios: Topic Pagination - 1, Topic Pagination - 2, Topic Pagination - 3
|
# Scenarios: Topic Pagination - 1, Topic Pagination - 2, Topic Pagination - 3
|
||||||
#
|
#
|
||||||
# GET /topics returns a page of topics ordered by their most recent post's
|
# GET /topics returns a page of topics ordered by their most recent post's
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# acceptance-mutation-manifest-begin
|
# acceptance-mutation-manifest-begin
|
||||||
# {"version":1,"tested_at":"2026-09-04T14:07:09.030166090Z","feature_name":"Topic Read","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/topic-read.feature","background_hash":"922fd45b8606f723073833f0aaca0e6d92b03a263a6b54c60490843e0ac41cc0","implementation_hash":"unknown","scenarios":[{"index":4,"name":"Topic Read - 5 GET /topics orders topics by most recent post","scenario_hash":"fcb48583a39be06c616d4d1636d06ac7e985a16fa7d9f66d9c9c43e95639812c","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-09-04T14:07:09.030166090Z"},{"index":0,"name":"Topic Read - 1 GET /topics lists distinct topics with their post counts","scenario_hash":"a425a0cbf18c7e7fcb7d01c3571556d4c184b013ad7990e724d95d8d100eebb9","mutation_count":8,"result":{"Total":8,"Killed":8,"Survived":0,"Errors":0},"tested_at":"2026-08-28T15:39:33.364455310Z"},{"index":1,"name":"Topic Read - 2 GET /topics/:room/posts returns the posts for a topic sorted by block height descending","scenario_hash":"cffb702052b0df29aa87510e0c45dd01b83c4607e7ce915bc0e660d601f79cce","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-28T15:39:33.364455310Z"}]}
|
# {"version":1,"tested_at":"2026-09-17T17:24:59.147244904Z","feature_name":"Topic Read","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/topic-read.feature","background_hash":"1a085ee8fae67e6f4b1217ee65d86b03af1088ca1a468da8a9edef83683332b3","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Topic Read - 1 GET /topics lists distinct topics with their post counts","scenario_hash":"a425a0cbf18c7e7fcb7d01c3571556d4c184b013ad7990e724d95d8d100eebb9","mutation_count":8,"result":{"Total":8,"Killed":8,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:24:59.147244904Z"},{"index":1,"name":"Topic Read - 2 GET /topics/:room/posts returns the posts for a topic sorted by block height descending","scenario_hash":"cffb702052b0df29aa87510e0c45dd01b83c4607e7ce915bc0e660d601f79cce","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:24:59.147244904Z"},{"index":4,"name":"Topic Read - 5 GET /topics orders topics by most recent post","scenario_hash":"fcb48583a39be06c616d4d1636d06ac7e985a16fa7d9f66d9c9c43e95639812c","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:24:59.147244904Z"}]}
|
||||||
# acceptance-mutation-manifest-end
|
# acceptance-mutation-manifest-end
|
||||||
|
|
||||||
# Scenarios: Topic Read - 1, Topic Read - 2, Topic Read - 3, Topic Read - 4, Topic Read - 5
|
# Scenarios: Topic Read - 1, Topic Read - 2, Topic Read - 3, Topic Read - 4, Topic Read - 5
|
||||||
|
|||||||
@@ -170,5 +170,5 @@ class TopicQuery {
|
|||||||
export default TopicQuery
|
export default TopicQuery
|
||||||
|
|
||||||
// mutate4javascript-manifest-begin
|
// mutate4javascript-manifest-begin
|
||||||
// {"version":1,"tested_at":"2026-09-04T20:12:29.631Z","module_hash":"c839533977040f56c6f45a1aa1e64ee422567570556c6beac19fe72d0c5970f0","functions":[{"id":"func/TopicQuery.constructor","name":"TopicQuery.constructor","line":16,"end_line":35,"hash":"56e5538d08449cf5870754c0e1e440b7d7e6a90ae5320b678fee77b18abcb81a"},{"id":"func/TopicQuery.roomFromKey","name":"TopicQuery.roomFromKey","line":37,"end_line":40,"hash":"4175916ac2bb8f9f102c70750f336ed980db2a30a1f8f35645534303408aaece"},{"id":"func/TopicQuery.txidFromKey","name":"TopicQuery.txidFromKey","line":42,"end_line":45,"hash":"4f5f5c456f1e74a60acff54d1d5e2d982be53886c2a77c6eff3293133efa9c87"},{"id":"func/TopicQuery.listTopics","name":"TopicQuery.listTopics","line":47,"end_line":76,"hash":"b96bd821ff6eb1e14abe4418004f74418d3276666ca185c544c90d2095b08474"},{"id":"func/TopicQuery.getTopicPostTxids","name":"TopicQuery.getTopicPostTxids","line":78,"end_line":98,"hash":"943729090c4532333410ee001c75ecdd45a562dfffd6f1f6d5e9313489f1b76a"},{"id":"func/TopicQuery.isFollowingRoom","name":"TopicQuery.isFollowingRoom","line":101,"end_line":110,"hash":"a0cf48cc6f4842adf65830b41d536cd6a078c7adf6b837e57259491157e2e97d"},{"id":"func/TopicQuery.listRoomFollowers","name":"TopicQuery.listRoomFollowers","line":113,"end_line":124,"hash":"351d71881e8b0953366056f223a6f06326e8ecd946277abc372ae5771c0b0f08"},{"id":"func/TopicQuery.followAddrFromValue","name":"TopicQuery.followAddrFromValue","line":126,"end_line":130,"hash":"2ce68c35f539e1fd7694774059b4b2afd096014c99a87c7538f3cbb70e26c3c7"}]}
|
// {"version":1,"tested_at":"2026-09-17T17:27:18.416Z","module_hash":"a31f16e3a18a841a71910ab28019ded60ef8800bc08dcd5460dfb21a84194903","functions":[{"id":"func/TopicQuery.constructor","name":"TopicQuery.constructor","line":24,"end_line":52,"hash":"51f22d20b4d0bb817f3b005e12f1635a3669a77bd741c3f0b386bc71fa28c08e"},{"id":"func/TopicQuery.roomFromKey","name":"TopicQuery.roomFromKey","line":54,"end_line":57,"hash":"4175916ac2bb8f9f102c70750f336ed980db2a30a1f8f35645534303408aaece"},{"id":"func/TopicQuery.txidFromKey","name":"TopicQuery.txidFromKey","line":59,"end_line":62,"hash":"4f5f5c456f1e74a60acff54d1d5e2d982be53886c2a77c6eff3293133efa9c87"},{"id":"func/TopicQuery.roomRange","name":"TopicQuery.roomRange","line":67,"end_line":69,"hash":"ceb99a638ee22ce625c9962d90786bd779de8d946b501640db9b76276c8bd7ea"},{"id":"func/TopicQuery.summaryRoom","name":"TopicQuery.summaryRoom","line":73,"end_line":76,"hash":"8ca2c04ae70839ca8e2deebe6f0661883dacbf3a06731dd9c134f326bf921b7c"},{"id":"func/TopicQuery.recencyRoom","name":"TopicQuery.recencyRoom","line":80,"end_line":84,"hash":"677abdcb0687362d170da432571cb0912bc43d4bd6e7022e3d785714f8bd9402"},{"id":"func/TopicQuery.listTopics","name":"TopicQuery.listTopics","line":90,"end_line":117,"hash":"75cb4c5ee90a44a156119f08226d0d3a8a75c136568676cdbd484c3f1e55a943"},{"id":"func/TopicQuery.getTopicPostTxids","name":"TopicQuery.getTopicPostTxids","line":119,"end_line":137,"hash":"c18e8ca08fa3695cc2d49bbc0a83eb5488831bf14ddfdb9c421afefd4b91d521"},{"id":"func/TopicQuery.isFollowingRoom","name":"TopicQuery.isFollowingRoom","line":140,"end_line":149,"hash":"a0cf48cc6f4842adf65830b41d536cd6a078c7adf6b837e57259491157e2e97d"},{"id":"func/TopicQuery.listRoomFollowers","name":"TopicQuery.listRoomFollowers","line":152,"end_line":161,"hash":"40dc014f433895e930bfec5de78d73ca2f75a023cecb174ddc57f7ec150dd300"},{"id":"func/TopicQuery.followAddrFromValue","name":"TopicQuery.followAddrFromValue","line":163,"end_line":167,"hash":"2ce68c35f539e1fd7694774059b4b2afd096014c99a87c7538f3cbb70e26c3c7"}]}
|
||||||
// mutate4javascript-manifest-end
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -150,5 +150,5 @@ class TopicsRESTControllerLib {
|
|||||||
export default TopicsRESTControllerLib
|
export default TopicsRESTControllerLib
|
||||||
|
|
||||||
// mutate4javascript-manifest-begin
|
// mutate4javascript-manifest-begin
|
||||||
// {"version":1,"tested_at":"2026-08-28T19:53:31.127Z","module_hash":"4c3f3e9152899ed272cb583e558c4522c0f54c37a0ae728f3ec6bc40248a3fdc","functions":[{"id":"func/TopicsRESTControllerLib.constructor","name":"TopicsRESTControllerLib.constructor","line":8,"end_line":23,"hash":"bacd230f3777dda39ec134a69a9a7c1801cb3cd09c0cc86659df324540053028"},{"id":"func/TopicsRESTControllerLib.handleError","name":"TopicsRESTControllerLib.handleError","line":25,"end_line":32,"hash":"b9ba0c7b9752ac2fda3cf058a983a8b6708715e2e4dc882903ab5ef369956e03"},{"id":"func/TopicsRESTControllerLib.getTopics","name":"TopicsRESTControllerLib.getTopics","line":49,"end_line":55,"hash":"b89a696bcd1d601ef86754974dbf181baf2ece7d592674ba4fd87887725d6fa8"},{"id":"func/TopicsRESTControllerLib.getTopicPosts","name":"TopicsRESTControllerLib.getTopicPosts","line":82,"end_line":90,"hash":"8ad5f9b1749968c3fbfea95683bb388a5d99eda0b8fe7a4dce3447d6d47e6dba"},{"id":"func/TopicsRESTControllerLib.getTopicFollowState","name":"TopicsRESTControllerLib.getTopicFollowState","line":110,"end_line":118,"hash":"6d72fecc2bf46276cd00d39cc1c19ed6647beed1f2f3ba187a2c085ed4308611"},{"id":"func/TopicsRESTControllerLib.getTopicFollowers","name":"TopicsRESTControllerLib.getTopicFollowers","line":136,"end_line":143,"hash":"2bcd22520d962c008a3255f5cf6cf536fdeb738d036d3c6b6d61d5d5cf040615"}]}
|
// {"version":1,"tested_at":"2026-09-17T16:42:30.815Z","module_hash":"50c5f72fa562d8c8f2c0ce8bed281a727b7effd3effbf92f8c4d573003b7b689","functions":[{"id":"func/TopicsRESTControllerLib.constructor","name":"TopicsRESTControllerLib.constructor","line":8,"end_line":23,"hash":"bacd230f3777dda39ec134a69a9a7c1801cb3cd09c0cc86659df324540053028"},{"id":"func/TopicsRESTControllerLib.handleError","name":"TopicsRESTControllerLib.handleError","line":25,"end_line":27,"hash":"801609a06e8f034702fb7293f5c7b0d316a54dfac1921d934e248a3b0a11f58c"},{"id":"func/TopicsRESTControllerLib.getTopics","name":"TopicsRESTControllerLib.getTopics","line":49,"end_line":56,"hash":"e2cf9b957874c42489d8c50b1d371f8191ce94d1002bca4a5ee7d6333d278d11"},{"id":"func/TopicsRESTControllerLib.getTopicPosts","name":"TopicsRESTControllerLib.getTopicPosts","line":84,"end_line":94,"hash":"bc18f51006b1c1079a0584b75d6666bec0b299febdcd565aab071679a2f1fe00"},{"id":"func/TopicsRESTControllerLib.getTopicFollowState","name":"TopicsRESTControllerLib.getTopicFollowState","line":114,"end_line":122,"hash":"6d72fecc2bf46276cd00d39cc1c19ed6647beed1f2f3ba187a2c085ed4308611"},{"id":"func/TopicsRESTControllerLib.getTopicFollowers","name":"TopicsRESTControllerLib.getTopicFollowers","line":140,"end_line":147,"hash":"2bcd22520d962c008a3255f5cf6cf536fdeb738d036d3c6b6d61d5d5cf040615"}]}
|
||||||
// mutate4javascript-manifest-end
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -85,3 +85,7 @@ export async function backfillTopicIndexes ({ roomsDb, topicSummariesDb, topicRe
|
|||||||
|
|
||||||
return { rooms: summaries.size }
|
return { rooms: summaries.size }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mutate4javascript-manifest-begin
|
||||||
|
// {"version":1,"tested_at":"2026-09-17T16:40:51.620Z","module_hash":"7a67d2c6ca7ea2494b1ff3a6dcc703c47a0dfd341fe2d593750b82b810f36da1","functions":[{"id":"func/topicRecencyKey","name":"topicRecencyKey","line":26,"end_line":29,"hash":"a034a6023f72d08e40d28bb51e41e370c1a42d5682315b162a7c2a81f7f31310"},{"id":"func/roomFromEntry","name":"roomFromEntry","line":33,"end_line":36,"hash":"5bf2a8338a623f65191729bc2433a87869b085b29547fbb5e42ebaa67d6473c1"},{"id":"func/applyPost","name":"applyPost","line":39,"end_line":43,"hash":"4edf9dd98a5889c62fd60f4107563427f0a55dc0250c27f6ac0af49b6b934c42"},{"id":"func/collectSummaries","name":"collectSummaries","line":45,"end_line":60,"hash":"73a07286477939379fb82ceace71991aa0cd830f041897dbe4cb5748c091a93f"},{"id":"func/removeStale","name":"removeStale","line":63,"end_line":69,"hash":"09cc44be72b9f955722f17c117d8ca18fe953dcf2cbd6efab110798dce4d0417"},{"id":"func/backfillTopicIndexes","name":"backfillTopicIndexes","line":71,"end_line":87,"hash":"7a6c1207032b16c5e7fc90fe9f7e2543b22d126fc0924296573c911dbf21d8c4"}]}
|
||||||
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ class ListTopics extends ListUseCase {
|
|||||||
export default ListTopics
|
export default ListTopics
|
||||||
|
|
||||||
// mutate4javascript-manifest-begin
|
// mutate4javascript-manifest-begin
|
||||||
// {"version":1,"tested_at":"2026-08-28T15:09:18.069Z","module_hash":"4d7755c0ac3ab79ea014862e6c728fde8af6e76c65046c9691f81ecd00993734","functions":[{"id":"func/ListTopics.constructor","name":"ListTopics.constructor","line":8,"end_line":10,"hash":"0e6872a32236c0cb076d7a728a37d8a40f2aca9bada0395d502cd3dba466c273"},{"id":"func/ListTopics.execute","name":"ListTopics.execute","line":12,"end_line":15,"hash":"c41d34757567d55659cc3a8ba77730a34478dfff9a95af00868aa8c5b2db159a"}]}
|
// {"version":1,"tested_at":"2026-09-17T16:41:57.476Z","module_hash":"07fb13faa0cead7ef6f073eab4f16dd74ece5eb290c37b306827735fa154320d","functions":[{"id":"func/ListTopics.constructor","name":"ListTopics.constructor","line":9,"end_line":11,"hash":"0e6872a32236c0cb076d7a728a37d8a40f2aca9bada0395d502cd3dba466c273"},{"id":"func/ListTopics.execute","name":"ListTopics.execute","line":13,"end_line":17,"hash":"b54242b912b2b499a9e0023a2428c193c0a2c9989e4b3187d61ad76c8e31ec47"}]}
|
||||||
// mutate4javascript-manifest-end
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# acceptance-mutation-manifest-begin
|
||||||
|
# {"version":1,"tested_at":"2026-09-17T16:45:30.497089416Z","feature_name":"Topic Recency Indexing","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-indexer/specs/topic-recency-indexing.feature","background_hash":"0639f3e7176ddd178bf266b5f62c66fcfff9acda7487916f3496de2a3dd86e3f","implementation_hash":"unknown","scenarios":[]}
|
||||||
|
# acceptance-mutation-manifest-end
|
||||||
|
|
||||||
# Scenarios: Topic Recency Indexing - 1, Topic Recency Indexing - 2, Topic Recency Indexing - 3, Topic Recency Indexing - 4, Topic Recency Indexing - 5
|
# Scenarios: Topic Recency Indexing - 1, Topic Recency Indexing - 2, Topic Recency Indexing - 3, Topic Recency Indexing - 4, Topic Recency Indexing - 5
|
||||||
#
|
#
|
||||||
# The indexer maintains two topic indexes so the read side can list topics by
|
# The indexer maintains two topic indexes so the read side can list topics by
|
||||||
|
|||||||
@@ -105,5 +105,5 @@ export function postChildKey (parentTxid, childTxid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mutate4javascript-manifest-begin
|
// mutate4javascript-manifest-begin
|
||||||
// {"version":1,"tested_at":"2026-08-28T04:22:49.741Z","module_hash":"9799b30443df95c47881bcc6a74bf9024e65741f94938af1c92dd5fa0180005f","functions":[{"id":"func/logProcessError","name":"logProcessError","line":7,"end_line":13,"hash":"78f39be271917cad97072ed5e2d1f531a30a37bc9c10169516e0457951c438f4"},{"id":"func/utf8FromPush","name":"utf8FromPush","line":15,"end_line":17,"hash":"356fc665a6389e392ec83522210dd95b2af6be341d00dc5edc0bbabfbd511d9e"},{"id":"func/stripLeadingEmptyPushes","name":"stripLeadingEmptyPushes","line":22,"end_line":28,"hash":"be7c401e07e4ef3670428afe7211860038c69da17abcd03508ea5cb640b46163"},{"id":"func/normalizeTwoPushMemoDatas","name":"normalizeTwoPushMemoDatas","line":34,"end_line":46,"hash":"61d050380c2cef7eed8870a66e347f1d01c56246d733d95d692b436cfb4a20ed"},{"id":"func/txHashFromPush","name":"txHashFromPush","line":48,"end_line":55,"hash":"704934308365cf0fbbb72aa437faf5db20a69bd57a16ba37b19c905ddc922ea9"},{"id":"func/followKey","name":"followKey","line":57,"end_line":59,"hash":"e1c5cfdd78afd3945e18e4af06e443efb306c27b07e3b3534c918ef000da9696"},{"id":"func/roomKey","name":"roomKey","line":61,"end_line":63,"hash":"d4f146c7a938bb6b30b13f071609a3ac4e96a26f116e321b804c16114884cd15"},{"id":"func/postHeightKey","name":"postHeightKey","line":65,"end_line":68,"hash":"25bdb7995aeb2823d0f808812251c3fd8c163089ea32b4f39ddf55520e03cbb1"},{"id":"func/addrPostHeightKey","name":"addrPostHeightKey","line":70,"end_line":73,"hash":"b9b6b08e4c5892905dc6577ad22dd48c13598ca205b1b8c13d7cc9622962879f"},{"id":"func/postLikeKey","name":"postLikeKey","line":75,"end_line":77,"hash":"627ce4b3ca6a9d28fbaa50bc1f51973509e7d8b52eda9208343dac1d936d7f51"},{"id":"func/postChildKey","name":"postChildKey","line":79,"end_line":81,"hash":"4d30329db33e34b0461bd1ba70b078fbef285a338f17f7e4d1166ca5bd107893"}]}
|
// {"version":1,"tested_at":"2026-09-17T16:25:37.308Z","module_hash":"00fa2f64726246ec4e403d93b7cabaf0fb5e2753614533c0b3651b23d6147749","functions":[{"id":"func/logProcessError","name":"logProcessError","line":7,"end_line":13,"hash":"78f39be271917cad97072ed5e2d1f531a30a37bc9c10169516e0457951c438f4"},{"id":"func/utf8FromPush","name":"utf8FromPush","line":15,"end_line":17,"hash":"356fc665a6389e392ec83522210dd95b2af6be341d00dc5edc0bbabfbd511d9e"},{"id":"func/stripLeadingEmptyPushes","name":"stripLeadingEmptyPushes","line":22,"end_line":28,"hash":"be7c401e07e4ef3670428afe7211860038c69da17abcd03508ea5cb640b46163"},{"id":"func/normalizeTwoPushMemoDatas","name":"normalizeTwoPushMemoDatas","line":34,"end_line":46,"hash":"61d050380c2cef7eed8870a66e347f1d01c56246d733d95d692b436cfb4a20ed"},{"id":"func/txHashFromPush","name":"txHashFromPush","line":48,"end_line":55,"hash":"704934308365cf0fbbb72aa437faf5db20a69bd57a16ba37b19c905ddc922ea9"},{"id":"func/followKey","name":"followKey","line":57,"end_line":59,"hash":"e1c5cfdd78afd3945e18e4af06e443efb306c27b07e3b3534c918ef000da9696"},{"id":"func/roomKey","name":"roomKey","line":61,"end_line":63,"hash":"d4f146c7a938bb6b30b13f071609a3ac4e96a26f116e321b804c16114884cd15"},{"id":"func/topicRecencyKey","name":"topicRecencyKey","line":68,"end_line":71,"hash":"ce7146b8dfca80309058c1512818fe2edc9f1b3658cbe1e06aabf78f65136b90"},{"id":"func/isNotFound","name":"isNotFound","line":75,"end_line":77,"hash":"662978032673c9bac6d64c519899ebfed72e477ea39b9fde19039ffa01de183d"},{"id":"func/getIfPresent","name":"getIfPresent","line":80,"end_line":87,"hash":"b012b3ef1d48f45a65fcbd4ff9574443cc98d2c2f7d12a504af74fc3e0d3f5af"},{"id":"func/postHeightKey","name":"postHeightKey","line":89,"end_line":92,"hash":"25bdb7995aeb2823d0f808812251c3fd8c163089ea32b4f39ddf55520e03cbb1"},{"id":"func/addrPostHeightKey","name":"addrPostHeightKey","line":94,"end_line":97,"hash":"b9b6b08e4c5892905dc6577ad22dd48c13598ca205b1b8c13d7cc9622962879f"},{"id":"func/postLikeKey","name":"postLikeKey","line":99,"end_line":101,"hash":"627ce4b3ca6a9d28fbaa50bc1f51973509e7d8b52eda9208343dac1d936d7f51"},{"id":"func/postChildKey","name":"postChildKey","line":103,"end_line":105,"hash":"4d30329db33e34b0461bd1ba70b078fbef285a338f17f7e4d1166ca5bd107893"}]}
|
||||||
// mutate4javascript-manifest-end
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -28,3 +28,7 @@ export async function handleTopicFollow (ctx) {
|
|||||||
await ensureTopicRoom(adapters, room)
|
await ensureTopicRoom(adapters, room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mutate4javascript-manifest-begin
|
||||||
|
// {"version":1,"tested_at":"2026-09-17T16:34:56.427Z","module_hash":"98bbf2e83e9afae81b2a25b3b03d66a32c9af7864e8eace4aec99b16dfe773b5","functions":[{"id":"func/handleTopicFollow","name":"handleTopicFollow","line":5,"end_line":30,"hash":"6137b8d03c0bcf24ddd08824e462565407677303981b1a376b94e449897e217a"}]}
|
||||||
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -48,3 +48,7 @@ export async function ensureTopicRoom (adapters, room) {
|
|||||||
await adapters.topicSummaryDb.update(room, { room, postCount: 0, lastHeight: 0 })
|
await adapters.topicSummaryDb.update(room, { room, postCount: 0, lastHeight: 0 })
|
||||||
await adapters.topicRecencyDb.update(topicRecencyKey(0, room), { room, blockHeight: 0 })
|
await adapters.topicRecencyDb.update(topicRecencyKey(0, room), { room, blockHeight: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mutate4javascript-manifest-begin
|
||||||
|
// {"version":1,"tested_at":"2026-09-17T16:22:46.190Z","module_hash":"cdd90074000f9eddb01ce298351d75ae8afdecc8fe34711a0a415099d64a7e70","functions":[{"id":"func/recordTopicPost","name":"recordTopicPost","line":19,"end_line":40,"hash":"91b542cddca534356bcaf15426ec2abccd04d610cf8db4c21de7deb216b250d8"},{"id":"func/ensureTopicRoom","name":"ensureTopicRoom","line":44,"end_line":50,"hash":"ca3cb6538d16deea7546a5bbf1a24a752f803a8929b7da8c3b8ba9582a20816a"}]}
|
||||||
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -36,3 +36,7 @@ export async function handleTopicMessage (ctx) {
|
|||||||
await recordTopicPost(adapters, room, blockHeight)
|
await recordTopicPost(adapters, room, blockHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mutate4javascript-manifest-begin
|
||||||
|
// {"version":1,"tested_at":"2026-09-17T16:35:45.388Z","module_hash":"3bf8a10d51b15167c500fd9d0586eedffb4b6f2191b6f563e9f9b15c75e850c0","functions":[{"id":"func/handleTopicMessage","name":"handleTopicMessage","line":6,"end_line":38,"hash":"d58559b106ed4f846feabd05070886a2962d95bbe55f2438e8d3f42b0f26a5ed"}]}
|
||||||
|
// mutate4javascript-manifest-end
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
normalizeTwoPushMemoDatas,
|
normalizeTwoPushMemoDatas,
|
||||||
stripLeadingEmptyPushes,
|
stripLeadingEmptyPushes,
|
||||||
txHashFromPush,
|
txHashFromPush,
|
||||||
logProcessError
|
logProcessError,
|
||||||
|
topicRecencyKey,
|
||||||
|
isNotFound
|
||||||
} from '../../../../src/use-cases/action-types/helpers.js'
|
} from '../../../../src/use-cases/action-types/helpers.js'
|
||||||
import { PREFIX_SET_PROFILE_PIC, PREFIX_POST } from '../../../../src/lib/memo-codes.js'
|
import { PREFIX_SET_PROFILE_PIC, PREFIX_POST } from '../../../../src/lib/memo-codes.js'
|
||||||
|
|
||||||
@@ -120,4 +122,22 @@ describe('#action-types/helpers', () => {
|
|||||||
assert.equal(create.firstCall.args[1].blockHeight, 600100)
|
assert.equal(create.firstCall.args[1].blockHeight, 600100)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#topicRecencyKey', () => {
|
||||||
|
it('should encode an inverted, zero-padded height with the room name', () => {
|
||||||
|
assert.equal(topicRecencyKey(0, 'bitcoin'), '999999999999:bitcoin')
|
||||||
|
assert.equal(topicRecencyKey(600200, 'bitcoin'), '999999399799:bitcoin')
|
||||||
|
assert.equal(topicRecencyKey(undefined, 'dev'), '999999999999:dev')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#isNotFound', () => {
|
||||||
|
it('should recognize not-found errors and reject everything else', () => {
|
||||||
|
assert.isTrue(isNotFound({ notFound: true }))
|
||||||
|
assert.isTrue(isNotFound({ code: 'LEVEL_NOT_FOUND' }))
|
||||||
|
assert.isTrue(isNotFound({ response: { status: 404 } }))
|
||||||
|
assert.isFalse(isNotFound({}))
|
||||||
|
assert.isFalse(isNotFound(null))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
import { recordTopicPost } from '../../../../src/use-cases/action-types/topic-indexing.js'
|
||||||
|
import { topicRecencyKey } from '../../../../src/use-cases/action-types/helpers.js'
|
||||||
|
|
||||||
|
function makeDb () {
|
||||||
|
const store = new Map()
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
async get (key) {
|
||||||
|
if (!store.has(key)) {
|
||||||
|
const err = new Error('not found')
|
||||||
|
err.notFound = true
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
return store.get(key)
|
||||||
|
},
|
||||||
|
async update (key, value) {
|
||||||
|
store.set(key, value)
|
||||||
|
},
|
||||||
|
async delete (key) {
|
||||||
|
store.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAdapters () {
|
||||||
|
return {
|
||||||
|
topicSummaryDb: makeDb(),
|
||||||
|
topicRecencyDb: makeDb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// These cover the defensive height fallbacks in recordTopicPost. The system
|
||||||
|
// always writes a numeric lastHeight, but a missing blockHeight or a legacy
|
||||||
|
// summary without lastHeight must still land at height zero.
|
||||||
|
describe('#recordTopicPost height fallbacks', () => {
|
||||||
|
it('should treat a missing block height as height zero', async () => {
|
||||||
|
const adapters = makeAdapters()
|
||||||
|
|
||||||
|
await recordTopicPost(adapters, 'bitcoin', undefined)
|
||||||
|
|
||||||
|
assert.deepEqual(adapters.topicSummaryDb.store.get('bitcoin'), {
|
||||||
|
room: 'bitcoin',
|
||||||
|
postCount: 1,
|
||||||
|
lastHeight: 0
|
||||||
|
})
|
||||||
|
assert.deepEqual(adapters.topicRecencyDb.store.get(topicRecencyKey(0, 'bitcoin')), {
|
||||||
|
room: 'bitcoin',
|
||||||
|
blockHeight: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the zero-height recency record when a legacy summary has no lastHeight', async () => {
|
||||||
|
const adapters = makeAdapters()
|
||||||
|
adapters.topicSummaryDb.store.set('bitcoin', { room: 'bitcoin', postCount: 2 })
|
||||||
|
adapters.topicRecencyDb.store.set(topicRecencyKey(0, 'bitcoin'), { room: 'bitcoin', blockHeight: 0 })
|
||||||
|
|
||||||
|
await recordTopicPost(adapters, 'bitcoin', 600100)
|
||||||
|
|
||||||
|
assert.isFalse(adapters.topicRecencyDb.store.has(topicRecencyKey(0, 'bitcoin')))
|
||||||
|
assert.deepEqual(adapters.topicRecencyDb.store.get(topicRecencyKey(600100, 'bitcoin')), {
|
||||||
|
room: 'bitcoin',
|
||||||
|
blockHeight: 600100
|
||||||
|
})
|
||||||
|
assert.deepEqual(adapters.topicSummaryDb.store.get('bitcoin'), {
|
||||||
|
room: 'bitcoin',
|
||||||
|
postCount: 3,
|
||||||
|
lastHeight: 600100
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the stale recency record when the next height equals the legacy fallback', async () => {
|
||||||
|
const adapters = makeAdapters()
|
||||||
|
adapters.topicSummaryDb.store.set('bitcoin', { room: 'bitcoin', postCount: 2 })
|
||||||
|
adapters.topicRecencyDb.store.set(topicRecencyKey(0, 'bitcoin'), { room: 'bitcoin', blockHeight: 0 })
|
||||||
|
|
||||||
|
await recordTopicPost(adapters, 'bitcoin', 1)
|
||||||
|
|
||||||
|
assert.isFalse(adapters.topicRecencyDb.store.has(topicRecencyKey(0, 'bitcoin')))
|
||||||
|
assert.deepEqual(adapters.topicRecencyDb.store.get(topicRecencyKey(1, 'bitcoin')), {
|
||||||
|
room: 'bitcoin',
|
||||||
|
blockHeight: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { assert } from 'chai'
|
import { assert } from 'chai'
|
||||||
import { handleTopicMessage } from '../../../../src/use-cases/action-types/topic-message.js'
|
import { handleTopicMessage } from '../../../../src/use-cases/action-types/topic-message.js'
|
||||||
import { topicRecencyKey } from '../../../../src/use-cases/action-types/helpers.js'
|
import { topicRecencyKey } from '../../../../src/use-cases/action-types/helpers.js'
|
||||||
|
import { MAX_POST_SIZE } from '../../../../src/lib/memo-codes.js'
|
||||||
|
|
||||||
function makeDb () {
|
function makeDb () {
|
||||||
const store = new Map()
|
const store = new Map()
|
||||||
@@ -155,6 +156,27 @@ describe('#handleTopicMessage topic indexes', () => {
|
|||||||
assert.equal(adapters.topicRecencyDb.store.size, 0)
|
assert.equal(adapters.topicRecencyDb.store.size, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should index a topic message with room and message exactly at the maximum size', async () => {
|
||||||
|
const adapters = makeAdapters()
|
||||||
|
const room = 'bitcoin'
|
||||||
|
const message = 'x'.repeat(MAX_POST_SIZE - room.length)
|
||||||
|
|
||||||
|
await processTopicMessage(adapters, {
|
||||||
|
txid: 'topic-max',
|
||||||
|
room,
|
||||||
|
addr: 'bitcoincash:qaddr-a',
|
||||||
|
height: 600100,
|
||||||
|
text: message
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(adapters.processErrorDb.store.size, 0)
|
||||||
|
assert.deepEqual(adapters.topicSummaryDb.store.get(room), {
|
||||||
|
room,
|
||||||
|
postCount: 1,
|
||||||
|
lastHeight: 600100
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should log a process error and index nothing for an oversized topic message', async () => {
|
it('should log a process error and index nothing for an oversized topic message', async () => {
|
||||||
const adapters = makeAdapters()
|
const adapters = makeAdapters()
|
||||||
const prefix = Buffer.from('6d0c', 'hex')
|
const prefix = Buffer.from('6d0c', 'hex')
|
||||||
|
|||||||
Reference in New Issue
Block a user