mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Add posting-state coverage and persist mutation state
Cover the new-post posting flag (initial idle, in-flight true, settled false) to kill the four surviving mutants, and commit the differential and acceptance mutation manifests written by the approved tools during the architect verification pass for the new-post-page feature. By architect.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Architectural Review Summary — new-post-page
|
||||
|
||||
## Task and commits reviewed
|
||||
- Task: `new-post-page`
|
||||
- Reviewed the merged branch ending at `d57975c466` (refactorer), which carried:
|
||||
- `9385034`/`4fa92d1` — specifier New Post Page Gherkin spec (`specs/memo-new.feature`)
|
||||
- `05f393c` — coder implementation (`src/services/new-post.js`, page wiring)
|
||||
- `d57975c` — refactorer property tests for memo post / new post invariants
|
||||
- Merged into `swarmforge-architect` (fast-forward) and processed as a batch.
|
||||
|
||||
## Architectural findings and fixes applied
|
||||
Reviewed UI/Core separation, dependency rule, information hiding/encapsulation, and
|
||||
local code quality.
|
||||
|
||||
1. **Testable controller behind adapters (good).** `src/services/new-post.js` is a
|
||||
testable controller wrapping `memo-post.js`; it holds draft input, the remaining-
|
||||
character counter, typed validation/length errors, and feed navigation. `memoPost`
|
||||
and `navigate` are injected so the module stays free of UI/IO concerns. Dependency
|
||||
direction is inward.
|
||||
2. **Unified acceptance handlers (good).** `acceptance/lib/handlers.js` now drives both
|
||||
`post-memo.feature` and `memo-new.feature` through one `world.newPage`, reusing the
|
||||
shared Memo post behavior. Wording differences are normalized via shared regex
|
||||
alternations; no step logic is duplicated.
|
||||
3. **Property tests (good).** `test/property/{harness,memo-post.property.test}.js`
|
||||
assert seeded invariants (validation classification across the length boundary,
|
||||
counter conservation `remaining === MAX - len`, `setInput` round-trip, menu-link
|
||||
idempotence). Kept separate from unit tests as the architecture requires.
|
||||
4. **Fix applied — posting-state coverage.** The language mutation tool surfaced 4
|
||||
survivors in `new-post.js`, all around the `posting` state flag (initial value and
|
||||
its true/false transitions in `submit`). Added unit coverage asserting the page
|
||||
starts idle, is `posting=true` while a submit is in flight (via a deferred wallet),
|
||||
and returns to `posting=false` on success and on error. This killed all 4 survivors.
|
||||
|
||||
## Verification results
|
||||
- **Unit (`node --test`):** 18/18 pass (added 2 posting-state tests).
|
||||
- **Property (`npm run test:property`):** 6/6 pass.
|
||||
- **Acceptance (normal):** both `memo-new` and `post-memo` generated suites pass.
|
||||
- **Mutation (`mutate4javascript`, `--max-workers 8`):**
|
||||
- `memo-post.js`: differential reuse (module unchanged since its 7/7 kill).
|
||||
- `new-post.js`: after coverage fix, **killed 11 / survived 0 / uncovered 0**
|
||||
(was 4 survivors before the fix).
|
||||
- **DRY (`dry4javascript src`):** no duplicate candidates.
|
||||
- **Gherkin acceptance mutation (soft):**
|
||||
- `memo-new.feature`: 11 executed — **5 killed, 6 survived**, 0 errors.
|
||||
- Killed: empty-memo boundary; character-counter `count` example values (217/212/0)
|
||||
— proving those values are connected to behavior.
|
||||
- Survived (documented equivalents): message-content dithers (4) plus case/length-
|
||||
neutral dithers in the counter scenario — message text is opaque data; only length
|
||||
affects the counter, so these don't change observable behavior.
|
||||
- `post-memo.feature`: empty-memo scenario reused as killed; remaining 5 executed are
|
||||
the same documented message-content equivalents.
|
||||
- Property tests: run separately via `npm run test:property`.
|
||||
|
||||
## Suite status
|
||||
- Unit + property + acceptance all pass; source-level mutation fully kills both testable
|
||||
core modules. Gherkin acceptance mutation survivors are documented equivalents.
|
||||
|
||||
## Handoffs sent
|
||||
- `git_handoff` → coder, refactorer (priority `00`, task `new-post-page`), to review the
|
||||
architect commit (posting-state coverage + tool manifests).
|
||||
|
||||
By architect.
|
||||
@@ -1,3 +1,7 @@
|
||||
# acceptance-mutation-manifest-begin
|
||||
# {"version":1,"tested_at":"2026-08-26T00:08:15.433121898Z","feature_name":"New Post Page","feature_path":"/home/trout/work/psf-memo-client/.worktrees/architect/specs/memo-new.feature","background_hash":"e1d5f81f1ed083ac6934c429ca3cb4a0f8d4dac44c2eaa45c0960920bde2c017","implementation_hash":"unknown","scenarios":[{"index":1,"name":"New Post Page - 2 an empty memo is rejected on the new post page","scenario_hash":"ac70dcf123f435da2b8a6c4953b0b22b8e7a5a8a74291547b954cb562cf9f339","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-08-26T00:08:15.433121898Z"}]}
|
||||
# acceptance-mutation-manifest-end
|
||||
|
||||
# Scenarios: New Post Page - 1, New Post Page - 2, New Post Page - 3, New Post Page - 4, New Post Page - 5
|
||||
Feature: New Post Page
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# acceptance-mutation-manifest-begin
|
||||
# {"version":1,"tested_at":"2026-08-25T23:22:59.013044939Z","feature_name":"Post a Memo","feature_path":"/home/trout/work/psf-memo-client/.worktrees/architect/specs/post-memo.feature","background_hash":"d7f1a31b7651301ec01bdea9c4c990f9a032ce179aa84f8d6a78595f8a476474","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Post a Memo - 2 an empty memo is rejected","scenario_hash":"4e6fea6fa5adbf7fe0eefdd9bc21a7027fe7312d2937a4fd68f1a1eb68d33a8d","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-08-25T23:22:52.631790256Z"}]}
|
||||
# {"version":1,"tested_at":"2026-08-26T00:08:35.176401424Z","feature_name":"Post a Memo","feature_path":"/home/trout/work/psf-memo-client/.worktrees/architect/specs/post-memo.feature","background_hash":"d7f1a31b7651301ec01bdea9c4c990f9a032ce179aa84f8d6a78595f8a476474","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Post a Memo - 2 an empty memo is rejected","scenario_hash":"4e6fea6fa5adbf7fe0eefdd9bc21a7027fe7312d2937a4fd68f1a1eb68d33a8d","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-08-25T23:22:52.631790256Z"}]}
|
||||
# acceptance-mutation-manifest-end
|
||||
|
||||
# Scenarios: Post a Memo - 1, Post a Memo - 2, Post a Memo - 3
|
||||
|
||||
@@ -97,5 +97,5 @@ MemoPost.MAX_MEMO_CHARS = MAX_MEMO_CHARS
|
||||
module.exports = MemoPost
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-08-25T23:18:27.154Z","module_hash":"139dec671f2f59aad3c83f5c225c1f8eb1f56e4f662f2b674d2f6db06bf4de8b","functions":[{"id":"func/MemoPost.constructor","name":"MemoPost.constructor","line":25,"end_line":28,"hash":"73596685cdf614a4aa3bb3ab2ee2eec1c080e41ef8c56053a521eb07ca5c7d48"},{"id":"func/MemoPost.validate","name":"MemoPost.validate","line":32,"end_line":42,"hash":"2e45fb32d480e36e04ac61c3fb414849d9daa640c5ac366ee1363be4c3903fd0"},{"id":"func/MemoPost.post","name":"MemoPost.post","line":46,"end_line":69,"hash":"6a817a7eceb24e9e4eb9689345ea3ef6456e8b872bff00a0587bddae8060ead2"},{"id":"func/MemoPost._throwIfInvalid","name":"MemoPost._throwIfInvalid","line":72,"end_line":82,"hash":"e01932c7c343519cc8dd52d3e29b695783c6cdb7e84368e193140827c26bb39c"},{"id":"func/MemoPost._reflectPost","name":"MemoPost._reflectPost","line":85,"end_line":93,"hash":"36e9b77ac19b8a0c598e02f438c3d2ac1f6b7495cf6e28d9546d064ce63f861a"}]}
|
||||
// {"version":1,"tested_at":"2026-08-26T00:06:35.333Z","module_hash":"600c2edb145b16db5e313a2911fe164a2c08731346f2a67f52bca18827d8081e","functions":[{"id":"func/MemoPost.constructor","name":"MemoPost.constructor","line":23,"end_line":26,"hash":"73596685cdf614a4aa3bb3ab2ee2eec1c080e41ef8c56053a521eb07ca5c7d48"},{"id":"func/MemoPost.validate","name":"MemoPost.validate","line":30,"end_line":40,"hash":"2e45fb32d480e36e04ac61c3fb414849d9daa640c5ac366ee1363be4c3903fd0"},{"id":"func/MemoPost.post","name":"MemoPost.post","line":44,"end_line":67,"hash":"6a817a7eceb24e9e4eb9689345ea3ef6456e8b872bff00a0587bddae8060ead2"},{"id":"func/MemoPost._throwIfInvalid","name":"MemoPost._throwIfInvalid","line":70,"end_line":80,"hash":"e01932c7c343519cc8dd52d3e29b695783c6cdb7e84368e193140827c26bb39c"},{"id":"func/MemoPost._reflectPost","name":"MemoPost._reflectPost","line":83,"end_line":91,"hash":"36e9b77ac19b8a0c598e02f438c3d2ac1f6b7495cf6e28d9546d064ce63f861a"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -81,3 +81,7 @@ NewPostPage.NEW_POST_PATH = NEW_POST_PATH
|
||||
NewPostPage.RECENT_FEED_PATH = RECENT_FEED_PATH
|
||||
|
||||
module.exports = NewPostPage
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-08-26T00:07:51.843Z","module_hash":"469dfd90342f6bcacf5b89ed819620663e221e20832f6936c35c46f9681ebfdc","functions":[{"id":"func/NewPostPage.constructor","name":"NewPostPage.constructor","line":22,"end_line":33,"hash":"7c957fbaa2b8d4adb62c1bbf240749243696a68e71e7896aefdbb68437432cd8"},{"id":"func/NewPostPage.addMenuLink","name":"NewPostPage.addMenuLink","line":36,"end_line":39,"hash":"bac97164d2d70bfdb946c4d54e67983c43cad093bac558f1d169e1739ca97137"},{"id":"func/NewPostPage.hasMenuLink","name":"NewPostPage.hasMenuLink","line":42,"end_line":44,"hash":"7e1abf5d0833aaf3b3da3a024de9eb2b80da900b2930e832c7e92d77e0e50344"},{"id":"func/NewPostPage.setInput","name":"NewPostPage.setInput","line":47,"end_line":50,"hash":"595484662b7ca07ef5eef5cebbff06309242552d9b4d15687df02f260ba88244"},{"id":"func/NewPostPage.remainingCount","name":"NewPostPage.remainingCount","line":53,"end_line":55,"hash":"521ce4ed841f62099529b327f2245a9e94c09af2f67ed5d91839e52607bcea37"},{"id":"func/NewPostPage.submit","name":"NewPostPage.submit","line":59,"end_line":77,"hash":"f4ce743f5a4bb139615086165b25173641a9388af16b7527f2db243a1c6b596c"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -88,6 +88,8 @@ test('posting a valid memo broadcasts the Memo post prefix and navigates to the
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
// The page returns to an idle (not posting) state after success.
|
||||
assert.equal(page.posting, false)
|
||||
// Broadcast happened with the Memo post prefix and the exact message.
|
||||
assert.equal(wallet.broadcasts.length, 1)
|
||||
assert.equal(wallet.broadcasts[0].prefix, '6d02')
|
||||
@@ -108,6 +110,7 @@ test('posting an empty memo is rejected with a validation error and nothing is b
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.error, 'memo_validation')
|
||||
assert.equal(page.submitError, 'memo_validation')
|
||||
assert.equal(page.posting, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.length, 0)
|
||||
assert.deepEqual(navigations, [])
|
||||
@@ -122,11 +125,42 @@ test('posting an over-long memo is rejected with a length error and nothing is b
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.error, 'memo_length')
|
||||
assert.equal(page.submitError, 'memo_length')
|
||||
assert.equal(page.posting, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.length, 0)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
test('the new post page starts idle (not posting)', () => {
|
||||
const { page } = build()
|
||||
assert.equal(page.posting, false)
|
||||
})
|
||||
|
||||
test('posting is true while a submit is in flight and false once it settles', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
|
||||
// Defer the broadcast so we can observe the in-flight posting state.
|
||||
let resolveSend
|
||||
wallet.sendOpReturn = async () => new Promise((resolve) => { resolveSend = resolve })
|
||||
const page = new NewPostPage({
|
||||
memoPost: new MemoPost({ wallet, feed }),
|
||||
navigate: () => {}
|
||||
})
|
||||
page.setInput('hello memo')
|
||||
|
||||
assert.equal(page.posting, false)
|
||||
const pending = page.submit()
|
||||
assert.equal(page.posting, true)
|
||||
|
||||
// Yield until the async chain reaches the deferred sendOpReturn call.
|
||||
await new Promise((r) => setImmediate(r))
|
||||
assert.equal(typeof resolveSend, 'function')
|
||||
resolveSend('in-flight-txid')
|
||||
await pending
|
||||
assert.equal(page.posting, false)
|
||||
})
|
||||
|
||||
test('submitting without a memo post handler reports an error and does not navigate', async () => {
|
||||
const navigations = []
|
||||
const page = new NewPostPage({ navigate: (p) => navigations.push(p) })
|
||||
|
||||
Reference in New Issue
Block a user