From 8592db3dbcb0db9ed6fbd7d53f6297fa8fe23ee6 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 3 Sep 2026 09:34:51 -0700 Subject: [PATCH 1/4] Add gherkin-parser wrapper for APS Babashka task By coder. --- swarmforge/scripts/gherkin-parser | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 swarmforge/scripts/gherkin-parser diff --git a/swarmforge/scripts/gherkin-parser b/swarmforge/scripts/gherkin-parser new file mode 100755 index 0000000..9e053cd --- /dev/null +++ b/swarmforge/scripts/gherkin-parser @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Wrapper for the Babashka APS gherkin-parser task. +# Delegates to the project-local APS checkout under .swarmforge/tools/aps. +set -euo pipefail +if [ "$#" -ne 2 ]; then + echo "usage: gherkin-parser " >&2 + exit 2 +fi +FEATURE_FILE="$(realpath -m "$1")" +JSON_OUTPUT="$(realpath -m "$2")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APS_DIR="$(cd "$SCRIPT_DIR/../../.swarmforge/tools/aps" && pwd)" +cd "$APS_DIR" +exec bb gherkin-parser "$FEATURE_FILE" "$JSON_OUTPUT" From edbdbc6019e2583227a5bea0c5bfb298d4870ed5 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 3 Sep 2026 14:28:45 -0700 Subject: [PATCH 2/4] Record architect merge in briefing By specifier. --- specifier-prompt.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifier-prompt.md b/specifier-prompt.md index cb45f27..f07282c 100644 --- a/specifier-prompt.md +++ b/specifier-prompt.md @@ -359,6 +359,7 @@ At the end of each session, update this file: - Note the current `master` HEAD commit. - State the next feature to work on. -Current `master` HEAD: `d3903fa` (backlog cleared of all previously listed -features; direction set to front-end improvements to `psf-memo-client`). +Current `master` HEAD: `d99eda3` (merged architect's notifications refactor + +startup bloat check; verified db 315 passing + lint clean, client 239 passing + +lint clean + build succeeds). Next action: **ask the user for the next front-end improvement to spec**. From c2946177825429957ac2b35bed8c0f4da6e44ad0 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 3 Sep 2026 16:07:48 -0700 Subject: [PATCH 3/4] Fix profile page wallet address wiring so Follow button appears for other profiles Use the reactive bchWalletState.cashAddress as the viewer address source and include it in the profile load effect dependencies. Extract a small wallet-address selector so the behavior is unit-testable. By coder. --- .../src/components/app-body/profile/index.js | 16 +++-- .../src/services/profile-wallet.js | 19 ++++++ .../test/unit/profile-wallet.test.js | 60 +++++++++++++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 psf-memo-client/src/services/profile-wallet.js create mode 100644 psf-memo-client/test/unit/profile-wallet.test.js diff --git a/psf-memo-client/src/components/app-body/profile/index.js b/psf-memo-client/src/components/app-body/profile/index.js index 1899759..cab899d 100644 --- a/psf-memo-client/src/components/app-body/profile/index.js +++ b/psf-memo-client/src/components/app-body/profile/index.js @@ -11,6 +11,7 @@ import MemoDb from '../../../services/memo-db' import MemoFollow from '../../../services/memo-follow' import MemoMute from '../../../services/memo-mute' import ProfilePage from '../../../services/profile-page' +import { getViewerAddress } from '../../../services/profile-wallet' import PostReplyCount from '../../post-reply-count' import LikeButton from '../../post-feed/like-button' import PostThreadModal from '../../post-thread-modal' @@ -75,6 +76,10 @@ function Profile (props) { setThreadTxid(null) } + const wallet = appData?.wallet || null + const appProfiles = appData?.profiles || null + const myAddr = getViewerAddress(appData) + const runAction = async (method, failMsg) => { if (!profilePage || busy) return setBusy(true) @@ -98,12 +103,11 @@ function Profile (props) { try { const memoDb = new MemoDb() - const myAddr = appData?.wallet?.walletInfo?.cashAddress || null - const memoFollow = myAddr - ? new MemoFollow({ wallet: appData.wallet, profiles: appData.profiles }) + const memoFollow = myAddr && wallet + ? new MemoFollow({ wallet, profiles: appProfiles }) : null - const memoMute = myAddr - ? new MemoMute({ wallet: appData.wallet, profiles: appData.profiles }) + const memoMute = myAddr && wallet + ? new MemoMute({ wallet, profiles: appProfiles }) : null const page = new ProfilePage({ memoDb, addr, myAddr, memoFollow, memoMute }) @@ -132,7 +136,7 @@ function Profile (props) { setError('Missing profile address') setLoading(false) } - }, [addr, appData?.wallet, appData?.profiles]) + }, [addr, myAddr, wallet, appProfiles]) const showFollowButton = profilePage && profilePage.canFollow() && !profilePage.isFollowing() const showUnfollowButton = profilePage && profilePage.canFollow() && profilePage.isFollowing() diff --git a/psf-memo-client/src/services/profile-wallet.js b/psf-memo-client/src/services/profile-wallet.js new file mode 100644 index 0000000..a54df16 --- /dev/null +++ b/psf-memo-client/src/services/profile-wallet.js @@ -0,0 +1,19 @@ +/* + Wallet address selection for the profile page. + + The React app stores the authenticated wallet address in two places: + `appData.bchWalletState.cashAddress` (reactive state updated as the wallet + initializes) and `appData.wallet.walletInfo.cashAddress` (the wallet object + itself). Prefer the reactive state so the profile page re-loads when the + address becomes available. +*/ + +'use strict' + +function getViewerAddress (appData) { + return appData?.bchWalletState?.cashAddress || + appData?.wallet?.walletInfo?.cashAddress || + null +} + +module.exports = { getViewerAddress } diff --git a/psf-memo-client/test/unit/profile-wallet.test.js b/psf-memo-client/test/unit/profile-wallet.test.js new file mode 100644 index 0000000..2a5b793 --- /dev/null +++ b/psf-memo-client/test/unit/profile-wallet.test.js @@ -0,0 +1,60 @@ +/* + Unit tests for the profile wallet address selector. + + The profile page must show follow/mute controls only when the viewer's + wallet address is known. The React app tracks the loaded address both in the + wallet object and in `bchWalletState`; the selector prefers the reactive + state so the profile reloads when the address becomes available. +*/ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert/strict') +const { getViewerAddress } = require('../../src/services/profile-wallet') + +function makeAppData ({ bchCashAddress, walletCashAddress } = {}) { + return { + bchWalletState: { + cashAddress: bchCashAddress + }, + wallet: walletCashAddress + ? { walletInfo: { cashAddress: walletCashAddress } } + : null, + profiles: {} + } +} + +test('getViewerAddress prefers bchWalletState.cashAddress', () => { + const appData = makeAppData({ + bchCashAddress: 'bitcoincash:state-address', + walletCashAddress: 'bitcoincash:wallet-address' + }) + + assert.equal(getViewerAddress(appData), 'bitcoincash:state-address') +}) + +test('getViewerAddress falls back to wallet.walletInfo.cashAddress', () => { + const appData = makeAppData({ + bchCashAddress: undefined, + walletCashAddress: 'bitcoincash:wallet-address' + }) + + assert.equal(getViewerAddress(appData), 'bitcoincash:wallet-address') +}) + +test('getViewerAddress returns null when no wallet is loaded', () => { + const appData = makeAppData({}) + + assert.equal(getViewerAddress(appData), null) +}) + +test('getViewerAddress returns null when wallet lacks an address', () => { + const appData = { + bchWalletState: {}, + wallet: { walletInfo: {} }, + profiles: {} + } + + assert.equal(getViewerAddress(appData), null) +}) From 5c6bffd6de8b0d12d0f21a7e3a68a9ac0e18052a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 3 Sep 2026 16:13:15 -0700 Subject: [PATCH 4/4] Add getViewerAddress property tests for reactive precedence Pin the profile wallet-address selector invariants over broad input space: reactive-state precedence, wallet fallback, and null-on-absent. CRAP (3.0), DRY, and mutation scan assessed; no split needed and no manifests discarded. By refactorer. --- .../property/profile-wallet.property.test.js | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 psf-memo-client/test/property/profile-wallet.property.test.js diff --git a/psf-memo-client/test/property/profile-wallet.property.test.js b/psf-memo-client/test/property/profile-wallet.property.test.js new file mode 100644 index 0000000..179fd4b --- /dev/null +++ b/psf-memo-client/test/property/profile-wallet.property.test.js @@ -0,0 +1,111 @@ +/* + Property tests for the profile page wallet-address selector. + + The unit tests check getViewerAddress at a few fixed shapes. These + properties pin the precedence invariant over a broad input space: + + - precedence: whenever the reactive bchWalletState.cashAddress is a truthy + value it is returned, regardless of the wallet-field address. + - fallback: when the reactive state carries no truthy address, the wallet + object's walletInfo.cashAddress is used when present. + - absence: when neither source has an address, the result is null, never a + partial/empty object. +*/ + +'use strict' + +const test = require('node:test') +const { seededRandom, forAll, intGen } = require('./harness') +const { getViewerAddress } = require('../../src/services/profile-wallet') + +const rng = seededRandom(20260903) +const CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + +// Random cash-address-shaped string (never empty so it passes as truthy). +function addressGen () { + const len = intGen(rng, 40, 60)() + let out = 'bitcoincash:q' + for (let i = 0; i < len; i++) { + out += CHARS[Math.floor(rng() * CHARS.length)] + } + return out +} + +// Pick a "reactive state address" which may be falsy (undefined/null/'') or a +// real address string. +function stateAddrGen () { + const roll = rng() + if (roll < 0.2) return undefined + if (roll < 0.4) return null + if (roll < 0.55) return '' + return addressGen() +} + +// Build a wallet field: absent, present without an address, or present with a +// randomly chosen address. +function walletGen () { + const roll = rng() + if (roll < 0.25) return undefined + if (roll < 0.4) return { walletInfo: {} } + return { walletInfo: { cashAddress: addressGen() } } +} + +function buildAppData (stateAddr, wallet) { + return { + bchWalletState: { cashAddress: stateAddr }, + wallet, + profiles: {} + } +} + +function expectedGet (stateAddr, wallet) { + if (stateAddr) return stateAddr + if (wallet && wallet.walletInfo && wallet.walletInfo.cashAddress) { + return wallet.walletInfo.cashAddress + } + return null +} + +function fixtureGen () { + return () => ({ + stateAddr: stateAddrGen(), + wallet: walletGen() + }) +} + +const gen = fixtureGen() + +test('getViewerAddress matches the reactive-then-wallet precedence rule', async () => { + await forAll( + gen, + ({ stateAddr, wallet }) => { + const appData = buildAppData(stateAddr, wallet) + return getViewerAddress(appData) === expectedGet(stateAddr, wallet) + }, + { label: 'getViewerAddress precedence' } + ) +}) + +test('getViewerAddress prefers the reactive state whenever it is truthy', async () => { + await forAll( + gen, + ({ stateAddr, wallet }) => { + if (!stateAddr) return true + const appData = buildAppData(stateAddr, wallet) + return getViewerAddress(appData) === stateAddr + }, + { label: 'getViewerAddress reactive preference invariant' } + ) +}) + +test('getViewerAddress returns null when no wallet address is available', async () => { + await forAll( + gen, + ({ stateAddr, wallet }) => { + if (expectedGet(stateAddr, wallet) !== null) return true + const appData = buildAppData(stateAddr, wallet) + return getViewerAddress(appData) === null + }, + { label: 'getViewerAddress null-on-absent invariant' } + ) +})