From 4bf99e7db01621ed081ec063a51a16c0997f2f1a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 2 Sep 2026 12:20:37 -0700 Subject: [PATCH] Review and harden following feed: kill survivors, DRY parseRequiredString - Extract parseRequiredString into pagination.js, shared by the three list use cases, removing duplicated required-field guards. - Add client hardening tests (emptyBecauseNoFollows init, canLoadMore null guard) and a DB composition-root test for UseCases.start. - Refresh mutation manifests and Gherkin acceptance-mutation stamp. By architect. --- psf-memo-client/specs/following-feed.feature | 4 ++ .../src/services/following-feed-page.js | 4 ++ psf-memo-client/src/services/memo-db.js | 2 +- .../test/unit/following-feed-page.test.js | 20 +++++++ psf-memo-db/src/adapters/post-query.js | 2 +- .../controllers/rest-api/posts/controller.js | 4 ++ .../src/controllers/rest-api/posts/index.js | 4 ++ psf-memo-db/src/use-cases/index.js | 2 +- psf-memo-db/src/use-cases/lib/pagination.js | 11 +++- .../src/use-cases/list-following-feed.js | 17 ++---- .../src/use-cases/list-posts-by-addr.js | 15 +---- psf-memo-db/src/use-cases/list-topic-posts.js | 15 +---- psf-memo-db/test/unit/use-cases/index.unit.js | 60 +++++++++++++++++++ 13 files changed, 121 insertions(+), 39 deletions(-) create mode 100644 psf-memo-db/test/unit/use-cases/index.unit.js diff --git a/psf-memo-client/specs/following-feed.feature b/psf-memo-client/specs/following-feed.feature index f2df3e8..34200c3 100644 --- a/psf-memo-client/specs/following-feed.feature +++ b/psf-memo-client/specs/following-feed.feature @@ -1,3 +1,7 @@ +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-09-02T19:13:00.507312989Z","feature_name":"Following Feed","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/following-feed.feature","background_hash":"0d66780cb1b8e277f0ada40a8ffe336dec7a8eaf658f19d2ea344815fb9bf26c","implementation_hash":"unknown","scenarios":[]} +# acceptance-mutation-manifest-end + # Scenarios: Following Feed - 1, Following Feed - 2, Following Feed - 3, Following Feed - 4, Following Feed - 5, Following Feed - 6, Following Feed - 7 # # The Following feed shows top-level posts (replies excluded) authored only by diff --git a/psf-memo-client/src/services/following-feed-page.js b/psf-memo-client/src/services/following-feed-page.js index 45e6697..9d2a88c 100644 --- a/psf-memo-client/src/services/following-feed-page.js +++ b/psf-memo-client/src/services/following-feed-page.js @@ -52,3 +52,7 @@ class FollowingFeedPage { FollowingFeedPage.FOLLOWING_FEED_PATH = FOLLOWING_FEED_PATH module.exports = FollowingFeedPage + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T19:04:51.797Z","module_hash":"7754a521f2e17d3d290ecca2dd644364ec96585a123a9e5601154d85c3a60548","functions":[{"id":"func/FollowingFeedPage.constructor","name":"FollowingFeedPage.constructor","line":13,"end_line":19,"hash":"015246f47e903cfc98029de709dbf4b0c0a47999c2ad737acfa3f46c17d66381"},{"id":"func/FollowingFeedPage.getMyAddress","name":"FollowingFeedPage.getMyAddress","line":21,"end_line":23,"hash":"3e5d4ac4df379300933a772020528b4ecf4ed83c7386a066f5c270df81adcddd"},{"id":"func/FollowingFeedPage.load","name":"FollowingFeedPage.load","line":25,"end_line":41,"hash":"c8e64d2a4fcd714aff9fc23893e1d313c63c0c6b8048149d4d89772772938cca"},{"id":"func/FollowingFeedPage.canLoadMore","name":"FollowingFeedPage.canLoadMore","line":43,"end_line":45,"hash":"634983bcc6bbe560daad8326db0dd4bf31d5cb9e45c40112565351dceaf8e5d5"},{"id":"func/FollowingFeedPage.getPost","name":"FollowingFeedPage.getPost","line":47,"end_line":49,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-client/src/services/memo-db.js b/psf-memo-client/src/services/memo-db.js index 9813888..3b29c47 100644 --- a/psf-memo-client/src/services/memo-db.js +++ b/psf-memo-client/src/services/memo-db.js @@ -162,5 +162,5 @@ class MemoDb { export default MemoDb // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-29T03:07:44.409Z","module_hash":"f5be8c328e8a131cd5c2bb9170baf494dfaba0312d577cbf6d2d4b1c9b307bc7","functions":[{"id":"func/MemoDb.constructor","name":"MemoDb.constructor","line":9,"end_line":11,"hash":"188825ae5983840d4e1a4df182966ef012297c336ea3b763c87ea1c384654279"},{"id":"func/MemoDb.getRecentProfiles","name":"MemoDb.getRecentProfiles","line":13,"end_line":15,"hash":"cb819c72881b70f719f964c607f64be70b40377297c738897eee495bcf6dff4d"},{"id":"func/MemoDb.getRecentPosts","name":"MemoDb.getRecentPosts","line":17,"end_line":19,"hash":"7c0fd29c2fdc67a27a05a190e2e4294a698a1fe5223d8017ca82c4f890c2d094"},{"id":"func/MemoDb.getProfile","name":"MemoDb.getProfile","line":21,"end_line":23,"hash":"6a250c7205799b40dbe97b8f17ddaee6eace8cb435b0561d9ff8f5eac93d2b20"},{"id":"func/MemoDb.getProfilePic","name":"MemoDb.getProfilePic","line":25,"end_line":27,"hash":"d61dd5aebce32ef28b69ad56f2f7108d6b5f333a4d94cd83f05f01a09fa8f1d7"},{"id":"func/MemoDb.getName","name":"MemoDb.getName","line":29,"end_line":31,"hash":"3f841735b93180c6aeeb14a563bb6d4eb0ca5d395fb960267c8896b06c1e85f3"},{"id":"func/MemoDb.getFollowState","name":"MemoDb.getFollowState","line":33,"end_line":35,"hash":"a7e0ad2336b146a0748f43f14bbcb92aa8e2dbe784b205b27ca2ae2958954e76"},{"id":"func/MemoDb.getMuteState","name":"MemoDb.getMuteState","line":37,"end_line":39,"hash":"80dc5dd85911a4088b16f4a77ce076001226f8c55dc0ce259e3a3ad63e38576d"},{"id":"func/MemoDb.getMuted","name":"MemoDb.getMuted","line":41,"end_line":43,"hash":"79d8e81b255fcb50e8974a08e429ed3ec814bbac16726af71b4d33757e0d53fa"},{"id":"func/MemoDb.getTopics","name":"MemoDb.getTopics","line":45,"end_line":47,"hash":"8d392b8a4b1bef405a9872c2564c51d9343df7b1c628d016258ee1c165f713ef"},{"id":"func/MemoDb.getTopicPosts","name":"MemoDb.getTopicPosts","line":49,"end_line":51,"hash":"4cc1e13334b5b4a2b72942cbc5f3cce8493254628f7d51da33d7be9c60ef9ff7"},{"id":"func/MemoDb.getTopicFollowState","name":"MemoDb.getTopicFollowState","line":53,"end_line":55,"hash":"d943e39339d689a241251bdfc704b1d99b65ba463a37852afbfd2dad20070a95"},{"id":"func/MemoDb.getTopicFollowers","name":"MemoDb.getTopicFollowers","line":57,"end_line":59,"hash":"95c0f5dc416b2fe974150ba5d551cb81c11400ccffe0876d82ae68c52e23e9f1"},{"id":"func/MemoDb._getState","name":"MemoDb._getState","line":62,"end_line":70,"hash":"d1f571f2421e0c70795a5a12e381175c42b5d0b0c5db203ad852aeb218b40825"},{"id":"func/MemoDb._getList","name":"MemoDb._getList","line":73,"end_line":81,"hash":"11bb9f3afad183d5c37f662506d27c75e010364798f3d4b9646358165433a596"},{"id":"func/MemoDb.getRecent","name":"MemoDb.getRecent","line":84,"end_line":95,"hash":"1565fd17dced45fbf1b6d6fd6b8003521129bbd88046ff52a4eb61c17862530d"},{"id":"func/MemoDb.getPage","name":"MemoDb.getPage","line":98,"end_line":109,"hash":"986ec43d7699d642b25a9f4627aa5420cc685a247c3caa4671797778bc84e3b5"},{"id":"func/MemoDb.getLevelResource","name":"MemoDb.getLevelResource","line":112,"end_line":125,"hash":"c2fb7338918b1e69aa8a14f65ae687532c843f0641f04074947731691001e4ea"},{"id":"func/MemoDb.getPostsByAddr","name":"MemoDb.getPostsByAddr","line":127,"end_line":129,"hash":"45bbfef12cd8e35da1d71299153b2e54304664d5e3191c497f105ba17b134431"},{"id":"func/MemoDb.getPostThread","name":"MemoDb.getPostThread","line":131,"end_line":142,"hash":"ed5ef157b457f8d984ff6a9488a4dccf7a37b0ae65adb40005df90508426661f"}]} +// {"version":1,"tested_at":"2026-09-02T19:05:30.808Z","module_hash":"8b135684ab4778511f6dd0b6a6410bfd31d225bbcc2a1b58e37cfaa5db0de8b5","functions":[{"id":"func/MemoDb.constructor","name":"MemoDb.constructor","line":9,"end_line":11,"hash":"188825ae5983840d4e1a4df182966ef012297c336ea3b763c87ea1c384654279"},{"id":"func/MemoDb.getRecentProfiles","name":"MemoDb.getRecentProfiles","line":13,"end_line":15,"hash":"cb819c72881b70f719f964c607f64be70b40377297c738897eee495bcf6dff4d"},{"id":"func/MemoDb.getRecentPosts","name":"MemoDb.getRecentPosts","line":17,"end_line":19,"hash":"7c0fd29c2fdc67a27a05a190e2e4294a698a1fe5223d8017ca82c4f890c2d094"},{"id":"func/MemoDb.getProfile","name":"MemoDb.getProfile","line":21,"end_line":23,"hash":"6a250c7205799b40dbe97b8f17ddaee6eace8cb435b0561d9ff8f5eac93d2b20"},{"id":"func/MemoDb.getProfilePic","name":"MemoDb.getProfilePic","line":25,"end_line":27,"hash":"d61dd5aebce32ef28b69ad56f2f7108d6b5f333a4d94cd83f05f01a09fa8f1d7"},{"id":"func/MemoDb.getName","name":"MemoDb.getName","line":29,"end_line":31,"hash":"3f841735b93180c6aeeb14a563bb6d4eb0ca5d395fb960267c8896b06c1e85f3"},{"id":"func/MemoDb.getFollowState","name":"MemoDb.getFollowState","line":33,"end_line":35,"hash":"a7e0ad2336b146a0748f43f14bbcb92aa8e2dbe784b205b27ca2ae2958954e76"},{"id":"func/MemoDb.getMuteState","name":"MemoDb.getMuteState","line":37,"end_line":39,"hash":"80dc5dd85911a4088b16f4a77ce076001226f8c55dc0ce259e3a3ad63e38576d"},{"id":"func/MemoDb.getMuted","name":"MemoDb.getMuted","line":41,"end_line":43,"hash":"79d8e81b255fcb50e8974a08e429ed3ec814bbac16726af71b4d33757e0d53fa"},{"id":"func/MemoDb.getTopics","name":"MemoDb.getTopics","line":45,"end_line":47,"hash":"8d392b8a4b1bef405a9872c2564c51d9343df7b1c628d016258ee1c165f713ef"},{"id":"func/MemoDb.getTopicPosts","name":"MemoDb.getTopicPosts","line":49,"end_line":51,"hash":"4cc1e13334b5b4a2b72942cbc5f3cce8493254628f7d51da33d7be9c60ef9ff7"},{"id":"func/MemoDb.getTopicFollowState","name":"MemoDb.getTopicFollowState","line":53,"end_line":55,"hash":"d943e39339d689a241251bdfc704b1d99b65ba463a37852afbfd2dad20070a95"},{"id":"func/MemoDb.getTopicFollowers","name":"MemoDb.getTopicFollowers","line":57,"end_line":59,"hash":"95c0f5dc416b2fe974150ba5d551cb81c11400ccffe0876d82ae68c52e23e9f1"},{"id":"func/MemoDb.search","name":"MemoDb.search","line":61,"end_line":72,"hash":"541370ca00b2f5e4cb6835b0601564f0dae6199cbdaa77e9221b29f8492c4dab"},{"id":"func/MemoDb._getState","name":"MemoDb._getState","line":75,"end_line":83,"hash":"d1f571f2421e0c70795a5a12e381175c42b5d0b0c5db203ad852aeb218b40825"},{"id":"func/MemoDb._getList","name":"MemoDb._getList","line":86,"end_line":94,"hash":"11bb9f3afad183d5c37f662506d27c75e010364798f3d4b9646358165433a596"},{"id":"func/MemoDb.getRecent","name":"MemoDb.getRecent","line":97,"end_line":108,"hash":"1565fd17dced45fbf1b6d6fd6b8003521129bbd88046ff52a4eb61c17862530d"},{"id":"func/MemoDb.getPage","name":"MemoDb.getPage","line":111,"end_line":122,"hash":"986ec43d7699d642b25a9f4627aa5420cc685a247c3caa4671797778bc84e3b5"},{"id":"func/MemoDb.getLevelResource","name":"MemoDb.getLevelResource","line":125,"end_line":138,"hash":"c2fb7338918b1e69aa8a14f65ae687532c843f0641f04074947731691001e4ea"},{"id":"func/MemoDb.getPostsByAddr","name":"MemoDb.getPostsByAddr","line":140,"end_line":142,"hash":"45bbfef12cd8e35da1d71299153b2e54304664d5e3191c497f105ba17b134431"},{"id":"func/MemoDb.getPostThread","name":"MemoDb.getPostThread","line":144,"end_line":155,"hash":"ed5ef157b457f8d984ff6a9488a4dccf7a37b0ae65adb40005df90508426661f"},{"id":"func/MemoDb.getFollowingFeed","name":"MemoDb.getFollowingFeed","line":157,"end_line":159,"hash":"905b6d7c194d295070eca2eb13c11fda93392213cf2b1fd5cd994085184b8d93"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-client/test/unit/following-feed-page.test.js b/psf-memo-client/test/unit/following-feed-page.test.js index 9329145..02d1a3f 100644 --- a/psf-memo-client/test/unit/following-feed-page.test.js +++ b/psf-memo-client/test/unit/following-feed-page.test.js @@ -143,3 +143,23 @@ test('canLoadMore reflects pagination.hasMore', async () => { test('exposes the following feed path', () => { assert.equal(FollowingFeedPage.FOLLOWING_FEED_PATH, '/posts/following') }) + +test('constructor starts with emptyBecauseNoFollows false', () => { + const page = new FollowingFeedPage({ + memoDb: makeMemoDb([], {}), + wallet: makeWallet() + }) + + assert.equal(page.emptyBecauseNoFollows, false) +}) + +test('canLoadMore is false when pagination is absent', async () => { + const page = new FollowingFeedPage({ + memoDb: makeMemoDb([], null), + wallet: makeWallet() + }) + + await page.load() + + assert.equal(page.canLoadMore(), false) +}) diff --git a/psf-memo-db/src/adapters/post-query.js b/psf-memo-db/src/adapters/post-query.js index d515f1e..803768c 100644 --- a/psf-memo-db/src/adapters/post-query.js +++ b/psf-memo-db/src/adapters/post-query.js @@ -345,5 +345,5 @@ class PostQuery { export default PostQuery // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-29T14:30:59.518Z","module_hash":"48f80ca7bfd456c9cf4c373e5ed22db42826d44df7a720babbd4e6b8e80a8e76","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":14,"end_line":59,"hash":"d7104ee46a40c256bc93c795c04a71f9ff212d723480cbc75ec6d497a0995e84"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":61,"end_line":63,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":65,"end_line":67,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.addrPostHeightKey","name":"PostQuery.addrPostHeightKey","line":69,"end_line":71,"hash":"48579f08593cee36cc2f107c2526ac9def687b354cc5e54089defa3fd37117eb"},{"id":"func/PostQuery.postLikeKey","name":"PostQuery.postLikeKey","line":73,"end_line":75,"hash":"5d16caac2cf88932702b28f9d95927183f8904948eb8c16c7e8c672cd3a0780c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":77,"end_line":80,"hash":"0fd135c51089dcc9bd2f166bb9f28faaa6a03d7eb84cf03cb9b36e97cdb3139c"},{"id":"func/PostQuery.txidFromAddrPostHeight","name":"PostQuery.txidFromAddrPostHeight","line":82,"end_line":84,"hash":"314d5432292a78b273e3165343d3e09276600cbaf239f76bcd1b36fe5fcf5e10"},{"id":"func/PostQuery.txidFromKeyParts","name":"PostQuery.txidFromKeyParts","line":87,"end_line":90,"hash":"59fb8173599070095d87e5d6f56eeb999f79e75e4d3f3e435515e9db8ac71868"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":92,"end_line":94,"hash":"74621495a3affc6ef8688b9d6a814a91c99b22c86c348d261c952aad25df1661"},{"id":"func/PostQuery.isReply","name":"PostQuery.isReply","line":96,"end_line":104,"hash":"be2e3729bd5f05cbfbab3630678eb5c389bd04c616d53b1e0c48c852f4cb25b1"},{"id":"func/PostQuery.countRepliesForTxids","name":"PostQuery.countRepliesForTxids","line":107,"end_line":121,"hash":"16268cd2a0f8db020a099b704c3d5a3cea5daf6a0936dd1f8d85c38809f006aa"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":124,"end_line":134,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.countLikesForTxids","name":"PostQuery.countLikesForTxids","line":137,"end_line":152,"hash":"54e6e3e6467e72d9dd033c9861b3b9ef5e426a76aed7a13e8ac1a0f0389468a3"},{"id":"func/PostQuery.likeTxidFromPostLike","name":"PostQuery.likeTxidFromPostLike","line":154,"end_line":158,"hash":"7e07a9c278a9abae8f646b4f1950f88760f2d5c6445600a42fa42f36d029a45a"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":162,"end_line":174,"hash":"a82ca3b46d68426edbe25f176c4a6019ea5fb6abea4c5d5e84aff3b381f63c61"},{"id":"func/PostQuery.postTxidFromPostLike","name":"PostQuery.postTxidFromPostLike","line":176,"end_line":180,"hash":"da7f8d0c63dbc074fb25da1a31dde5075c2c3469b84a5c700bd4a2961879d8e9"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":183,"end_line":190,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":194,"end_line":202,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":204,"end_line":219,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxidsAndCount","name":"PostQuery.scanPostsByAddrTxidsAndCount","line":225,"end_line":253,"hash":"a872d7a1f71ce1ba6a1dee46a820989cbbb7287dfc433051be1e15890dbc010b"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":256,"end_line":259,"hash":"3498f2b9615e79b9472a5c7be26520c78db7c1661071c90a882f981d8acca477"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":261,"end_line":277,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":279,"end_line":290,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":293,"end_line":308,"hash":"6e12c0b66758cd0103ff6abad1379b3883bea82f084d2382991b4643dfb63360"}]} +// {"version":1,"tested_at":"2026-09-02T19:03:53.926Z","module_hash":"19d494a556bfcad4d1f49ed37ab693fea343df3c5e9161702905c852872f08d7","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":14,"end_line":60,"hash":"1588b3f2716a69f4470536875b0e7a0ef21d34ff6fb9312a2772e71f22d44c2c"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":62,"end_line":64,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":66,"end_line":68,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.addrPostHeightKey","name":"PostQuery.addrPostHeightKey","line":70,"end_line":72,"hash":"48579f08593cee36cc2f107c2526ac9def687b354cc5e54089defa3fd37117eb"},{"id":"func/PostQuery.postLikeKey","name":"PostQuery.postLikeKey","line":74,"end_line":76,"hash":"5d16caac2cf88932702b28f9d95927183f8904948eb8c16c7e8c672cd3a0780c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":78,"end_line":81,"hash":"0fd135c51089dcc9bd2f166bb9f28faaa6a03d7eb84cf03cb9b36e97cdb3139c"},{"id":"func/PostQuery.txidFromAddrPostHeight","name":"PostQuery.txidFromAddrPostHeight","line":83,"end_line":85,"hash":"314d5432292a78b273e3165343d3e09276600cbaf239f76bcd1b36fe5fcf5e10"},{"id":"func/PostQuery.txidFromKeyParts","name":"PostQuery.txidFromKeyParts","line":88,"end_line":91,"hash":"59fb8173599070095d87e5d6f56eeb999f79e75e4d3f3e435515e9db8ac71868"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":93,"end_line":95,"hash":"74621495a3affc6ef8688b9d6a814a91c99b22c86c348d261c952aad25df1661"},{"id":"func/PostQuery.isReply","name":"PostQuery.isReply","line":97,"end_line":105,"hash":"be2e3729bd5f05cbfbab3630678eb5c389bd04c616d53b1e0c48c852f4cb25b1"},{"id":"func/PostQuery.countRepliesForTxids","name":"PostQuery.countRepliesForTxids","line":108,"end_line":122,"hash":"16268cd2a0f8db020a099b704c3d5a3cea5daf6a0936dd1f8d85c38809f006aa"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":125,"end_line":135,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.countLikesForTxids","name":"PostQuery.countLikesForTxids","line":138,"end_line":153,"hash":"54e6e3e6467e72d9dd033c9861b3b9ef5e426a76aed7a13e8ac1a0f0389468a3"},{"id":"func/PostQuery.likeTxidFromPostLike","name":"PostQuery.likeTxidFromPostLike","line":155,"end_line":159,"hash":"7e07a9c278a9abae8f646b4f1950f88760f2d5c6445600a42fa42f36d029a45a"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":163,"end_line":175,"hash":"a82ca3b46d68426edbe25f176c4a6019ea5fb6abea4c5d5e84aff3b381f63c61"},{"id":"func/PostQuery.postTxidFromPostLike","name":"PostQuery.postTxidFromPostLike","line":177,"end_line":181,"hash":"da7f8d0c63dbc074fb25da1a31dde5075c2c3469b84a5c700bd4a2961879d8e9"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":184,"end_line":191,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":195,"end_line":203,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":205,"end_line":220,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxidsAndCount","name":"PostQuery.scanPostsByAddrTxidsAndCount","line":226,"end_line":254,"hash":"a872d7a1f71ce1ba6a1dee46a820989cbbb7287dfc433051be1e15890dbc010b"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":257,"end_line":260,"hash":"3498f2b9615e79b9472a5c7be26520c78db7c1661071c90a882f981d8acca477"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":262,"end_line":278,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":280,"end_line":291,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":294,"end_line":309,"hash":"6e12c0b66758cd0103ff6abad1379b3883bea82f084d2382991b4643dfb63360"},{"id":"func/PostQuery.scanFollowingFeedTxidsAndCount","name":"PostQuery.scanFollowingFeedTxidsAndCount","line":314,"end_line":342,"hash":"90920dc6ed0d082545edc8b860f2f6fcc13e632510b9ddf879a4e4c7efa0e652"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/src/controllers/rest-api/posts/controller.js b/psf-memo-db/src/controllers/rest-api/posts/controller.js index 64fa2ef..0332a3a 100644 --- a/psf-memo-db/src/controllers/rest-api/posts/controller.js +++ b/psf-memo-db/src/controllers/rest-api/posts/controller.js @@ -154,3 +154,7 @@ class PostsRESTControllerLib { } export default PostsRESTControllerLib + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T19:04:12.781Z","module_hash":"e9f06f6fdf475b85ec8ebd378f9c8784f38d0d05254d463f98b6df37f58c2886","functions":[{"id":"func/PostsRESTControllerLib.constructor","name":"PostsRESTControllerLib.constructor","line":8,"end_line":25,"hash":"ffe914078c8e659d1dc86fac2c236f5310aeddebffbf01c48d3c1907b2adb902"},{"id":"func/PostsRESTControllerLib.runUseCase","name":"PostsRESTControllerLib.runUseCase","line":31,"end_line":37,"hash":"116112d778cc231815bd7ac296e8e3f90fbc209e1c91892e9bec596ed0821863"},{"id":"func/PostsRESTControllerLib.listPostsForAddr","name":"PostsRESTControllerLib.listPostsForAddr","line":44,"end_line":48,"hash":"23c4480bc080ea2da6ca30cc36b78cc3b51357bf2cdb5927c1511c430b89e0c9"},{"id":"func/PostsRESTControllerLib.handleError","name":"PostsRESTControllerLib.handleError","line":50,"end_line":57,"hash":"57720513b9edacb2e38e6089ae5881c73cdc767c941977d8d979d2bf37513965"},{"id":"func/PostsRESTControllerLib.getRecentPosts","name":"PostsRESTControllerLib.getRecentPosts","line":86,"end_line":89,"hash":"cb96f5a491bc3f52e71ed5ff1bbc5ce1654bb38ec86dca0f00b407778645c322"},{"id":"func/PostsRESTControllerLib.getPostsByAddr","name":"PostsRESTControllerLib.getPostsByAddr","line":115,"end_line":117,"hash":"41283c2ead5c5015962daa1c094ed52db593750eb01094ce4dbe74c6ea979b70"},{"id":"func/PostsRESTControllerLib.getFollowingFeed","name":"PostsRESTControllerLib.getFollowingFeed","line":144,"end_line":146,"hash":"bba784a60d63043e039b341a6affb3029c1b1259f26479f078be93fef4fd6d3c"},{"id":"func/PostsRESTControllerLib.getPostThread","name":"PostsRESTControllerLib.getPostThread","line":148,"end_line":153,"hash":"d4a7873e7e024b861c7e1aebc98b4ebb563eab80d21f0027c94bda46fc9a20fb"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-db/src/controllers/rest-api/posts/index.js b/psf-memo-db/src/controllers/rest-api/posts/index.js index 713e087..3d26fca 100644 --- a/psf-memo-db/src/controllers/rest-api/posts/index.js +++ b/psf-memo-db/src/controllers/rest-api/posts/index.js @@ -34,3 +34,7 @@ class PostsRouter { } export default PostsRouter + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T19:04:31.503Z","module_hash":"640e7871f89083654c3a3d442b91cd29bc5f9f483f43bc3f521e7c874932a696","functions":[{"id":"func/PostsRouter.constructor","name":"PostsRouter.constructor","line":9,"end_line":24,"hash":"552bd82e3f00dd57053a773a52a72c40e522cb146e146bf3335cc119504e963e"},{"id":"func/PostsRouter.attach","name":"PostsRouter.attach","line":26,"end_line":33,"hash":"452b2740b90a4f51ac1b4fdde8c305b6a5e6e93e806acd4e5d37d2e9b940bbf1"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/index.js b/psf-memo-db/src/use-cases/index.js index 61ff709..df0a2f2 100644 --- a/psf-memo-db/src/use-cases/index.js +++ b/psf-memo-db/src/use-cases/index.js @@ -133,5 +133,5 @@ class UseCases { export default UseCases // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-29T03:34:22.727Z","module_hash":"bd1af3666615a359eb970d1576472fe7a792fbfd135972e81ba50f1441976e94","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":23,"end_line":48,"hash":"511ff53d6fe3084c42bc443e67183991e065fe6432d26291d8f44fcf3ee3fe94"},{"id":"func/UseCases.start","name":"UseCases.start","line":50,"end_line":118,"hash":"4a94f92a0c335ee536da858df44d13e81930b86df9e27ebff75d71753865e299"}]} +// {"version":1,"tested_at":"2026-09-02T19:03:33.402Z","module_hash":"c2846cfbc10b257fb2bc78b233698bcb3629774248ed55c01936745fa6542537","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":25,"end_line":52,"hash":"4c06a996b9753331ed29694f0c1939617e0da1a4255244aecd7fa2f4eb4521f3"},{"id":"func/UseCases.start","name":"UseCases.start","line":54,"end_line":130,"hash":"358ac54d6f521176f500c42ce8577b5f2fcc9051b6ae095e4d5b07cc35a7b81a"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/lib/pagination.js b/psf-memo-db/src/use-cases/lib/pagination.js index dc425a8..ea4476b 100644 --- a/psf-memo-db/src/use-cases/lib/pagination.js +++ b/psf-memo-db/src/use-cases/lib/pagination.js @@ -43,6 +43,15 @@ export function parseOffset (offset) { return parsed } +// Validate a required string field (e.g. an address or room) shared by the +// addr- and room-scoped list use cases. +export function parseRequiredString (value, fieldName) { + if (!value || typeof value !== 'string') { + throw httpError(`${fieldName} is required`, 400) + } + return value +} + export function attachReplyCounts (posts, replyCounts) { return posts.map((post) => ({ ...post, @@ -72,5 +81,5 @@ export function assemblePostPage ({ posts, replyCounts, likeCounts, total, limit } // mutate4javascript-manifest-begin -// {"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"}]} +// {"version":1,"tested_at":"2026-09-02T19:03:14.637Z","module_hash":"31610c64eb700d95f93b85fd90d35ef9c34ec4ad386478534134db76cd227171","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/parseRequiredString","name":"parseRequiredString","line":48,"end_line":53,"hash":"0073c76aefb8129d225c86da88824381da8e9f6c956b5d2fa01485b65c83ea47"},{"id":"func/attachReplyCounts","name":"attachReplyCounts","line":55,"end_line":60,"hash":"99b9e818ac032752eb76fc9fcdcba0dcd1d94bee830921131a18fd01a4848f76"},{"id":"func/attachLikeCounts","name":"attachLikeCounts","line":62,"end_line":67,"hash":"eebdaa4235d209f5ab3cfb7c242bb76b29c0365676107eee7f681ecb1c188ea8"},{"id":"func/assemblePostPage","name":"assemblePostPage","line":69,"end_line":81,"hash":"496cbbf9f16d3b36ffbe486a62462cbb939f02b9f1610e4a577d2e5ff167c51f"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/list-following-feed.js b/psf-memo-db/src/use-cases/list-following-feed.js index 8dbb6ca..e2771e0 100644 --- a/psf-memo-db/src/use-cases/list-following-feed.js +++ b/psf-memo-db/src/use-cases/list-following-feed.js @@ -5,7 +5,7 @@ viewer's own posts are excluded. Results are paginated with limit/offset. */ -import { parseLimit, parseOffset, assemblePostPage } from './lib/pagination.js' +import { parseLimit, parseOffset, parseRequiredString, assemblePostPage } from './lib/pagination.js' import { ListUseCase } from './lib/use-case.js' class ListFollowingFeed extends ListUseCase { @@ -16,17 +16,8 @@ class ListFollowingFeed extends ListUseCase { } } - parseAddr (addr) { - if (!addr || typeof addr !== 'string') { - const err = new Error('addr is required') - err.status = 400 - throw err - } - return addr - } - async execute (inObj = {}) { - const addr = this.parseAddr(inObj.addr) + const addr = parseRequiredString(inObj.addr, 'addr') const limit = parseLimit(inObj.limit) const offset = parseOffset(inObj.offset) @@ -47,3 +38,7 @@ class ListFollowingFeed extends ListUseCase { } export default ListFollowingFeed + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T19:02:18.559Z","module_hash":"df53fbf714d93da7638df954aba33dd2605b4ce42a8f09fe0ea11d32c3f65eb6","functions":[{"id":"func/ListFollowingFeed.constructor","name":"ListFollowingFeed.constructor","line":12,"end_line":17,"hash":"b62fd6f9f182ccdd9797918d8d662d1c753d24c019a0401a0011c76296112a6d"},{"id":"func/ListFollowingFeed.execute","name":"ListFollowingFeed.execute","line":19,"end_line":37,"hash":"c32d42cb050f69164bab44febe375ba73acfa2c8b39659c7e84c4bbc4146c17e"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/list-posts-by-addr.js b/psf-memo-db/src/use-cases/list-posts-by-addr.js index 4b206c4..ef8d6b0 100644 --- a/psf-memo-db/src/use-cases/list-posts-by-addr.js +++ b/psf-memo-db/src/use-cases/list-posts-by-addr.js @@ -3,7 +3,7 @@ Uses the postHeights secondary index for efficient sorting and pagination. */ -import { parseLimit, parseOffset, assemblePostPage } from './lib/pagination.js' +import { parseLimit, parseOffset, parseRequiredString, assemblePostPage } from './lib/pagination.js' import { ListUseCase } from './lib/use-case.js' class ListPostsByAddr extends ListUseCase { @@ -11,17 +11,8 @@ class ListPostsByAddr extends ListUseCase { super(localConfig, { useCaseName: 'ListPostsByAddr', adapterName: 'postQuery' }) } - parseAddr (addr) { - if (!addr || typeof addr !== 'string') { - const err = new Error('addr is required') - err.status = 400 - throw err - } - return addr - } - async execute (inObj = {}) { - const addr = this.parseAddr(inObj.addr) + const addr = parseRequiredString(inObj.addr, 'addr') const limit = parseLimit(inObj.limit) const offset = parseOffset(inObj.offset) @@ -39,5 +30,5 @@ class ListPostsByAddr extends ListUseCase { export default ListPostsByAddr // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-28T04:26:54.668Z","module_hash":"fb2f5f9d5132a1fba8d76483cf2cf6aa1e7478bb143d8eaba55789b0af39c8c6","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":"1275dac9f67add375fb09ec3b2e202217a03ead211bbd27bcff4607df5b7462e"}]} +// {"version":1,"tested_at":"2026-09-02T19:02:37.206Z","module_hash":"ca00e44ac02115d358d25a2932fe1084f39070c5754a9e96c27a37e215a5998e","functions":[{"id":"func/ListPostsByAddr.constructor","name":"ListPostsByAddr.constructor","line":10,"end_line":12,"hash":"f777f3685c5b2199ec3c3a7043b3cf288a9bbf583232bd1fc58cc346373f03d6"},{"id":"func/ListPostsByAddr.execute","name":"ListPostsByAddr.execute","line":14,"end_line":27,"hash":"c21b84d10ac7308f026f5967c6b1f2113357f8f3c96f600c38e2fc08e64f7caf"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/list-topic-posts.js b/psf-memo-db/src/use-cases/list-topic-posts.js index 78788cb..7b60035 100644 --- a/psf-memo-db/src/use-cases/list-topic-posts.js +++ b/psf-memo-db/src/use-cases/list-topic-posts.js @@ -3,7 +3,7 @@ (newest first), paginated. */ -import { parseLimit, parseOffset, attachReplyCounts, attachLikeCounts } from './lib/pagination.js' +import { parseLimit, parseOffset, parseRequiredString, attachReplyCounts, attachLikeCounts } from './lib/pagination.js' class ListTopicPosts { constructor (localConfig = {}) { @@ -21,17 +21,8 @@ class ListTopicPosts { this.execute = this.execute.bind(this) } - parseRoom (room) { - if (!room || typeof room !== 'string') { - const err = new Error('room is required') - err.status = 400 - throw err - } - return room - } - async execute (inObj = {}) { - const room = this.parseRoom(inObj.room) + const room = parseRequiredString(inObj.room, 'room') const limit = parseLimit(inObj.limit) const offset = parseOffset(inObj.offset) @@ -58,5 +49,5 @@ class ListTopicPosts { export default ListTopicPosts // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-28T15:16:11.032Z","module_hash":"42a65ea5f1a4dea88cd1c99bcf8ba8b476dc24a073459ffa26d21151179db078","functions":[{"id":"func/ListTopicPosts.constructor","name":"ListTopicPosts.constructor","line":9,"end_line":22,"hash":"cdc313ae9059c07ce5cd2440e49fc958b9d273ed5ab43b0a6b81fd4949ad9272"},{"id":"func/ListTopicPosts.parseRoom","name":"ListTopicPosts.parseRoom","line":24,"end_line":31,"hash":"6508b101a3e1c0a32fed8f54f5b0383f2a891e5aa5c59a6fe0d1af6c9ffb9f37"},{"id":"func/ListTopicPosts.execute","name":"ListTopicPosts.execute","line":33,"end_line":55,"hash":"3ed269937f0d908694ba94f2faef658b92d7160be1262abffcd268fe022c352c"}]} +// {"version":1,"tested_at":"2026-09-02T19:02:55.018Z","module_hash":"fc9d576550fdf3097c37feae14a9c1d7a8d15e44fa7c705140d5c869bc2a4f02","functions":[{"id":"func/ListTopicPosts.constructor","name":"ListTopicPosts.constructor","line":9,"end_line":22,"hash":"cdc313ae9059c07ce5cd2440e49fc958b9d273ed5ab43b0a6b81fd4949ad9272"},{"id":"func/ListTopicPosts.execute","name":"ListTopicPosts.execute","line":24,"end_line":46,"hash":"61f3386dac6823688ea4d0a6dbf3edebc77b36d3171e8a2661274d8871dcc39a"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/test/unit/use-cases/index.unit.js b/psf-memo-db/test/unit/use-cases/index.unit.js new file mode 100644 index 0000000..7d0dda0 --- /dev/null +++ b/psf-memo-db/test/unit/use-cases/index.unit.js @@ -0,0 +1,60 @@ +/* + Unit tests for the UseCases composition root. +*/ + +import { assert } from 'chai' +import UseCases from '../../../src/use-cases/index.js' + +describe('#UseCases', () => { + // Build a minimal adapters bundle that satisfies every use-case constructor + // so start() can instantiate the full set. + function makeAdapters () { + return { + postQuery: {}, + profileQuery: {}, + followQuery: { + listFollowing: () => [], + listFollowers: () => [] + }, + muteQuery: { + listMuted: () => [] + }, + topicQuery: {}, + searchQuery: {}, + pollQuery: {} + } + } + + describe('#constructor', () => { + it('throws when adapters are missing', () => { + assert.throws(() => new UseCases({}), /Adapters required/) + }) + }) + + describe('#start', () => { + it('instantiates every use case and returns true', async () => { + const uut = new UseCases({ adapters: makeAdapters() }) + const result = await uut.start() + + assert.equal(result, true) + assert.isNotNull(uut.listRecentProfiles) + assert.isNotNull(uut.listRecentPosts) + assert.isNotNull(uut.listPostsByAddr) + assert.isNotNull(uut.listFollowingFeed) + assert.isNotNull(uut.getPostThread) + assert.isNotNull(uut.followState) + assert.isNotNull(uut.listFollowing) + assert.isNotNull(uut.listFollowers) + assert.isNotNull(uut.muteState) + assert.isNotNull(uut.listMuted) + assert.isNotNull(uut.listTopics) + assert.isNotNull(uut.listTopicPosts) + assert.isNotNull(uut.topicFollowState) + assert.isNotNull(uut.listTopicFollowers) + assert.isNotNull(uut.getPoll) + assert.isNotNull(uut.getPollOptions) + assert.isNotNull(uut.getPollVotes) + assert.isNotNull(uut.searchAll) + }) + }) +})