diff --git a/docs/reviews/new-post-page-summary.md b/docs/reviews/new-post-page-summary.md new file mode 100644 index 0000000..a003fc1 --- /dev/null +++ b/docs/reviews/new-post-page-summary.md @@ -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. diff --git a/specs/memo-new.feature b/specs/memo-new.feature index 031e1a7..3c9377f 100644 --- a/specs/memo-new.feature +++ b/specs/memo-new.feature @@ -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 diff --git a/specs/post-memo.feature b/specs/post-memo.feature index d6743b1..bd62b23 100644 --- a/specs/post-memo.feature +++ b/specs/post-memo.feature @@ -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 diff --git a/src/services/memo-post.js b/src/services/memo-post.js index c954fe3..16100b3 100644 --- a/src/services/memo-post.js +++ b/src/services/memo-post.js @@ -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 diff --git a/src/services/new-post.js b/src/services/new-post.js index 2538ef3..8949544 100644 --- a/src/services/new-post.js +++ b/src/services/new-post.js @@ -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 diff --git a/test/unit/new-post.test.js b/test/unit/new-post.test.js index 61cba29..bdac616 100644 --- a/test/unit/new-post.test.js +++ b/test/unit/new-post.test.js @@ -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) })