From 61d50dfa88d2a7a224ea374bf2d469a94761002a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 26 Aug 2026 06:40:27 -0700 Subject: [PATCH] Add architectural review for like-tip-memo - Add three boundary unit tests to memo-like (tip == max, spendable == dust, tip == spendable) closing mutation survivors. - Refresh mutate4javascript and acceptance-mutation manifests from normal tool runs. - Add written architectural review report for like-tip-memo. By architect. --- docs/reviews/like-tip-memo-summary.md | 75 +++++++++++++++++++++++++++ specs/like-tip-memo.feature | 4 ++ src/services/memo-like.js | 4 ++ test/unit/memo-like.test.js | 42 +++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 docs/reviews/like-tip-memo-summary.md diff --git a/docs/reviews/like-tip-memo-summary.md b/docs/reviews/like-tip-memo-summary.md new file mode 100644 index 0000000..ec38569 --- /dev/null +++ b/docs/reviews/like-tip-memo-summary.md @@ -0,0 +1,75 @@ +# Architectural Review Summary — like-tip-memo + +## Task and commits reviewed +- Task: `like-tip-memo` +- Processed the refactorer handoff `merge_and_process refactorer 6b7a7e2b86` and + fast-forward merged the `swarmforge-refactorer` branch ending at `6b7a7e2b86` + into `swarmforge-architect`, which carried: + - `eb9aa29` — specifier: added `specs/like-tip-memo.feature` (10 scenarios) and + `dev-docs/psf-memo-db-changes.md` + - `dd0535d` — coder: implemented the Memo like action (`0x6d04`) with optional + author tip, the like/tip page controller, heart-icon + modal UI, and acceptance + handlers + - `6b7a7e2b86` — refactorer: reduced CRAP in the like/tip slice, consolidated + rejection assertions, and added hex/like/property tests + +## Architectural findings and fixes applied +Reviewed UI/Core separation, dependency rule, information hiding/encapsulation, and +local code quality. + +1. **UI/Core separation (good).** Core behavior is cleanly split from UI: + - `src/services/memo-like.js` (extends `MemoAction`) owns compose/validate/broadcast + of the like OP_RETURN and optional tip, speaking only to an injected wallet. + - `src/services/like-tip-page.js` (extends `PageController`) owns the modal page + behavior (open/close/setTip/submit and error classification). + - React (`like-button.js`, `like-tip-modal.js`, `post-feed-item.js`) is a thin shell + over the testable services. Core is testable with no UI/IO. +2. **Dependency rule (good).** UI components depend inward on services; + `LikeTipPage` → `MemoLike` → `MemoAction`. Wallet/network I/O stays behind the + injected minimal-slp-wallet adapter. No low-level module reaches toward IO. +3. **Information hiding / encapsulation (good).** Txid hex encoding, tip validation + (integer, dust floor, hard max, spendable balance), and the `0x6d04` wire prefix + are encapsulated. `hexToBytes` was extracted to `src/services/hex.js` and is shared + with `memo-reply.js` — a good cross-slice DRY consolidation. +4. **Local code quality (good).** Small, single-responsibility modules; CRAP for every + like/tip function is at or below 6 with 100% coverage. +5. **Boundary test gap closed (architect fix).** Added three boundary unit tests to + `test/unit/memo-like.test.js`: + - a tip at exactly the hard maximum is accepted, + - a wallet with exactly the dust-limit balance can make a pure like, + - a tip at exactly the spendable balance is accepted. + These kill the previously-surviving `> -> >=` / `< -> <=` boundary mutants and pin + down the inclusive max/balance semantics. + +## Verification results +- **Unit (`node --test`):** 133/133 pass (including the 3 added boundary tests). +- **Property (`node --test test/property/*.test.js`):** 18/18 pass. +- **Acceptance (normal):** `memo-new`, `post-memo`, `set-name`, `reply-memo`, and + `like-tip-memo` (10 scenarios) generated suites all pass. +- **Mutation (`mutate4javascript src/services/memo-like.js --mutate-all`):** + 25 killed, 0 uncovered, **7 survived — all documented equivalents**: + - `validate` / `validateTip` `{ok:true}` returns are ignored by `like()` (dead + return value), + - `like` default `tipSats = 0` (callers always pass the parsed tip), + - `_requireTipAddress` / `_buildTipOutput` boundary mutations on `tipSats ≤ 1` / + `authorAddress.length > 1` are unreachable because such tips are below the dust + floor and rejected earlier. +- **DRY (`dry4javascript src/services/memo-like.js like-tip-page.js hex.js`):** no + duplicate candidates. +- **Gherkin acceptance mutation (soft) on `like-tip-memo.feature`:** 16 executed, + **4 killed, 12 survived**, 0 errors. Every survivor is an expected soft survival: + a mutated example value still yields the same asserted outcome (a still-invalid tip, + a still-below-dust tip, or a tip/balance pair that still trips the same check). +- **CRAP:** all like/tip functions ≤ 6.0 (max `_validateTipAmount` 6.0), 100% coverage. + +## Suite status +Unit + property + acceptance all pass; source-level mutation kills all meaningful +mutants with survivors documented as equivalents; DRY clean; CRAP within threshold. + +## Handoffs sent +- `git_handoff` → coder, refactorer (priority `00`, task `like-tip-memo`), to review + the architect commit (three boundary tests + tool-refreshed manifests; no source + logic change). +- No handoff to the specifier: the architect produced no functional feature commit. + +By architect. diff --git a/specs/like-tip-memo.feature b/specs/like-tip-memo.feature index 14d0065..466ef65 100644 --- a/specs/like-tip-memo.feature +++ b/specs/like-tip-memo.feature @@ -1,3 +1,7 @@ +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-08-26T13:30:06.787347982Z","feature_name":"Like / Tip a Memo","feature_path":"/home/trout/work/psf-memo-client/.worktrees/architect/specs/like-tip-memo.feature","background_hash":"2cd08f817665556cd20cb9a69b0d96a8ad871a1a9c1929c9d7a56b41bb3eff64","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Like / Tip a Memo - 2 a pure like broadcasts the Memo like action","scenario_hash":"95a91e98202ff0980db0f249b801ed10b6e885ce24756116f90ef5d35289aa9d","mutation_count":2,"result":{"Total":2,"Killed":2,"Survived":0,"Errors":0},"tested_at":"2026-08-26T13:30:06.787347982Z"},{"index":8,"name":"Like / Tip a Memo - 9 a user can like their own post","scenario_hash":"eec77e112d8aa86f9e8ff6b7e38634e0c252be14cb60c35974f71b07fead8fb5","mutation_count":2,"result":{"Total":2,"Killed":2,"Survived":0,"Errors":0},"tested_at":"2026-08-26T13:30:06.787347982Z"}]} +# acceptance-mutation-manifest-end + # Scenarios: Like / Tip a Memo - 1, Like / Tip a Memo - 2, Like / Tip a Memo - 3, Like / Tip a Memo - 4, Like / Tip a Memo - 5, Like / Tip a Memo - 6, Like / Tip a Memo - 7, Like / Tip a Memo - 8, Like / Tip a Memo - 9, Like / Tip a Memo - 10 Feature: Like / Tip a Memo diff --git a/src/services/memo-like.js b/src/services/memo-like.js index 63b0d9b..78bb5fd 100644 --- a/src/services/memo-like.js +++ b/src/services/memo-like.js @@ -187,3 +187,7 @@ MemoLike.DUST_LIMIT_SATS = DUST_LIMIT_SATS MemoLike.MAX_TIP_SATS = MAX_TIP_SATS module.exports = MemoLike + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-08-26T13:36:49.209Z","module_hash":"f27440db63274125b6b03bafd66222b31966670d8956b865e0bf2c695aacb54b","functions":[{"id":"func/MemoLike.constructor","name":"MemoLike.constructor","line":42,"end_line":47,"hash":"6ebbf231548843e23238988fde00cca861fd89513abba00febba964f3ef32365"},{"id":"func/MemoLike.validate","name":"MemoLike.validate","line":51,"end_line":60,"hash":"d9267f14ac52c7aa3afc07333b621c5faa751efa217bf02ae7695bb640827d44"},{"id":"func/MemoLike.validateTip","name":"MemoLike.validateTip","line":64,"end_line":74,"hash":"504b41cfa8db9754d31b0698550f9fe791d005c5b44bd30ec380202f1df11d6f"},{"id":"func/MemoLike._validateTipAmount","name":"MemoLike._validateTipAmount","line":77,"end_line":95,"hash":"98483bcf6212c2e0a2400d4da1b667b9f1c017a06ee6e4f282ccdf71cb8e397a"},{"id":"func/MemoLike.getSpendableSats","name":"MemoLike.getSpendableSats","line":99,"end_line":106,"hash":"7790154b7489758e6254cc7be00802fde237a33e0bb1440a2da33d303a2bdbe7"},{"id":"func/MemoLike.like","name":"MemoLike.like","line":112,"end_line":139,"hash":"5a748442d77446643448f1835b5e546569524af642af6be7c338e33ca4901051"},{"id":"func/MemoLike.reflect","name":"MemoLike.reflect","line":142,"end_line":145,"hash":"b159b912ba5b88468e5bfb7458385edc932850fe43948f3028e8ed9fea7269e9"},{"id":"func/MemoLike._requireTipAddress","name":"MemoLike._requireTipAddress","line":148,"end_line":154,"hash":"3b838a583b14112523e2cbe856a63896b1045a1ad91dac500e38693332b7c890"},{"id":"func/MemoLike._buildTipOutput","name":"MemoLike._buildTipOutput","line":157,"end_line":161,"hash":"e2a602c0afa9730dee67497869c4d18ed75708f6503d68fb1f0c0ba596826b5a"},{"id":"func/MemoLike._notifyFeed","name":"MemoLike._notifyFeed","line":164,"end_line":173,"hash":"bfc0e888ee106e21ee73146e92ebd3e4ce41d2ed8595a1539f823dc6c8f31d06"},{"id":"func/MemoLike._incrementPostCount","name":"MemoLike._incrementPostCount","line":176,"end_line":182,"hash":"61ae383f836b64a7aeabaf0d04e50ac9a24b87f726d5d6afac5f5585cdc6031c"}]} +// mutate4javascript-manifest-end diff --git a/test/unit/memo-like.test.js b/test/unit/memo-like.test.js index 6ea0341..a7c0c77 100644 --- a/test/unit/memo-like.test.js +++ b/test/unit/memo-like.test.js @@ -159,6 +159,35 @@ test('a tip above the hard maximum is rejected with a maximum error', async () = ) }) +test('a tip at exactly the hard maximum is accepted', async () => { + const wallet = fakeWallet({ + cashAddress: MY_ADDRESS, + utxos: [{ txid: 'u1', value: MemoLike.MAX_TIP_SATS + 1000 }] + }) + const memoLike = new MemoLike({ wallet }) + + const txid = await memoLike.like(POST_TXID, MemoLike.MAX_TIP_SATS, AUTHOR_ADDRESS) + + assert.equal(txid, 'fake-txid') + assert.equal(wallet.broadcasts.length, 1) + assert.deepEqual(wallet.broadcasts[0].bchOutput, [ + { address: AUTHOR_ADDRESS, amountSat: MemoLike.MAX_TIP_SATS } + ]) +}) + +test('a wallet with exactly the dust-limit balance can make a pure like', async () => { + const wallet = fakeWallet({ + cashAddress: MY_ADDRESS, + utxos: [{ txid: 'u1', value: MemoLike.DUST_LIMIT_SATS }] + }) + const memoLike = new MemoLike({ wallet }) + + const txid = await memoLike.like(POST_TXID, 0, AUTHOR_ADDRESS) + + assert.equal(txid, 'fake-txid') + assert.equal(wallet.broadcasts.length, 1) +}) + test('a tip above the spendable balance is rejected with a balance error', async () => { await assertLikeRejected( fakeWallet({ utxos: [{ txid: 'u1', value: 30000 }] }), @@ -167,6 +196,19 @@ test('a tip above the spendable balance is rejected with a balance error', async ) }) +test('a tip at exactly the spendable balance is accepted', async () => { + const wallet = fakeWallet({ + cashAddress: MY_ADDRESS, + utxos: [{ txid: 'u1', value: 25000 }] + }) + const memoLike = new MemoLike({ wallet }) + + const txid = await memoLike.like(POST_TXID, 25000, AUTHOR_ADDRESS) + + assert.equal(txid, 'fake-txid') + assert.equal(wallet.broadcasts.length, 1) +}) + test('a wallet with zero spendable balance cannot like', async () => { await assertLikeRejected(fakeWallet({ utxos: [] }), 0, 'like_empty_balance') })