mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
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.
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user