Merge commit '164bdb3cba' into swarmforge-refactorer

This commit is contained in:
Chris Troutner
2026-08-25 17:33:53 -07:00
5 changed files with 113 additions and 11 deletions
+37 -2
View File
@@ -31,7 +31,9 @@ function makeWallet (address) {
return this.utxos
},
sendOpReturn: async function (walletInfo, bchUtxos, msg, prefix) {
// Record the broadcast attempt, then fail if configured to do so.
this.broadcasts.push({ walletInfo, bchUtxos, msg, prefix })
if (this.failWith) throw new Error(this.failWith)
return 'aa'.repeat(32)
}
}
@@ -95,6 +97,17 @@ const handlers = [
world.currentPath = NewPostPage.RECENT_FEED_PATH
}
},
{
name: 'wallet fails to broadcast with error',
pattern: /^the wallet fails to broadcast with the error "<([A-Za-z0-9_]+)>"$/,
run (m, example, world) {
const param = m[1]
if (!(param in example)) {
throw new Error(`Missing example value for "${param}"`)
}
world.wallet.failWith = example[param]
}
},
{
name: 'navigate to path',
pattern: /^I navigate to the path (.+)$/,
@@ -109,6 +122,16 @@ const handlers = [
}
}
},
{
name: 'remain on path',
pattern: /^I remain on the path (.+)$/,
run (m, example, world) {
const target = m[1].trim()
if (world.currentPath !== target) {
throw new Error(`Expected to remain on path ${target}, but current path is ${world.currentPath}.`)
}
}
},
{
name: 'open navigation menu',
pattern: /^I open the navigation menu$/,
@@ -145,8 +168,8 @@ const handlers = [
}
},
{
name: 'broadcasts OP_RETURN with Memo post prefix',
pattern: /^(?:the wallet|the app) broadcasts an OP_RETURN transaction with the Memo post prefix$/,
name: 'broadcasts/attempts OP_RETURN with Memo post prefix',
pattern: /^(?:the wallet|the app) (?:broadcasts|attempts to broadcast) an OP_RETURN transaction with the Memo post prefix$/,
run (m, example, world) {
const broadcasts = world.wallet.broadcasts
if (!broadcasts.length) {
@@ -176,6 +199,18 @@ const handlers = [
}
}
},
{
name: 'page shows error containing text',
pattern: /^the new post page shows an error containing "<([A-Za-z0-9_]+)>"$/,
run (m, example, world) {
const param = m[1]
const expected = example[param]
const actual = world.newPage.broadcastError || ''
if (!actual.includes(expected)) {
throw new Error(`Expected an error containing "${expected}", got "${actual}".`)
}
}
},
{
name: 'page shows validation/length error',
pattern: /^the (?:app|new post page) shows a (validation|length) error$/,
+15 -1
View File
@@ -2,7 +2,7 @@
# {"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
# Scenarios: New Post Page - 1, New Post Page - 2, New Post Page - 3, New Post Page - 4, New Post Page - 5, New Post Page - 6
Feature: New Post Page
Background:
@@ -59,3 +59,17 @@ Feature: New Post Page
Scenario: New Post Page - 5 the navigation menu links to the new post page
Given I open the navigation menu
Then the menu shows a link to the path /posts/new
Scenario Outline: New Post Page - 6 a failed broadcast surfaces the real error and the user stays on the page
Given I navigate to the path /posts/new
And the wallet fails to broadcast with the error "<broadcast_error>"
When I type a memo with the text "<message>"
When I click the post button
Then the app attempts to broadcast an OP_RETURN transaction with the Memo post prefix
Then the new post page shows an error containing "<broadcast_error>"
Then I remain on the path /posts/new
Examples:
| message | broadcast_error |
| hello memo | BCH UTXO list is empty |
| hello memo | Insufficient balance |
+9 -5
View File
@@ -36,11 +36,15 @@ function NewPost (props) {
const result = await page.submit()
if (!result.ok) {
setErr(
result.error === 'memo_length'
? `Memo is too long. Maximum is ${maxChars} characters.`
: 'Memo must not be empty.'
)
if (result.error === 'memo_length') {
setErr(`Memo is too long. Maximum is ${maxChars} characters.`)
} else if (result.error === 'memo_validation') {
setErr('Memo must not be empty.')
} else if (result.message) {
setErr(`Failed to broadcast: ${result.message}`)
} else {
setErr('Failed to post memo.')
}
}
// On success page.submit() navigated to the recent feed.
} catch (submitErr) {
+13 -3
View File
@@ -26,6 +26,7 @@ class NewPostPage {
this.input = ''
this.submitError = null
this.broadcastError = null
this.posting = false
// The navigation menu links to the new post page.
@@ -55,10 +56,12 @@ class NewPostPage {
}
// Validate and post the current draft. On success, navigate to the recent
// feed. On failure, record the typed error. Resolves with a result object.
// feed. On failure, record the typed error and stay on the page. Resolves
// with a result object.
async submit () {
this.posting = true
this.submitError = null
this.broadcastError = null
try {
if (!this.memoPost) {
@@ -70,9 +73,16 @@ class NewPostPage {
this.posting = false
return { ok: true, txid }
} catch (err) {
this.submitError = err.code || 'memo_validation'
if (err.code === 'memo_validation' || err.code === 'memo_length') {
// Local validation failure: record the typed validation error.
this.submitError = err.code
} else {
// Broadcast (or handler) failure: surface the real error message.
this.broadcastError = err.message || String(err)
this.submitError = 'broadcast'
}
this.posting = false
return { ok: false, error: this.submitError }
return { ok: false, error: this.submitError, message: this.broadcastError }
}
}
}
+39
View File
@@ -29,6 +29,7 @@ function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0
getUtxos: async function () { return this.utxos },
sendOpReturn: async function (walletInfo, bchUtxos, msg, prefix) {
this.broadcasts.push({ walletInfo, bchUtxos, msg, prefix })
if (this.failWith) throw new Error(this.failWith)
return 'newpost-txid'
}
}
@@ -171,3 +172,41 @@ test('submitting without a memo post handler reports an error and does not navig
assert.equal(result.ok, false)
assert.deepEqual(navigations, [])
})
test('a failed broadcast surfaces the real error and does not navigate', async () => {
const wallet = fakeWallet()
const feed = fakeFeed()
wallet.failWith = 'BCH UTXO list is empty'
const navigations = []
const page = new NewPostPage({
memoPost: new MemoPost({ wallet, feed }),
navigate: (p) => navigations.push(p)
})
page.setInput('hello memo')
const result = await page.submit()
assert.equal(result.ok, false)
assert.equal(page.submitError, 'broadcast')
assert.match(page.broadcastError, /BCH UTXO list is empty/)
// The broadcast was attempted (recorded) before it failed.
assert.equal(wallet.broadcasts.length, 1)
assert.equal(wallet.broadcasts[0].prefix, '6d02')
// The user stays on the page.
assert.deepEqual(navigations, [])
})
test('a failed broadcast surfaces a different real error message', async () => {
const wallet = fakeWallet()
wallet.failWith = 'Insufficient balance'
const page = new NewPostPage({
memoPost: new MemoPost({ wallet }),
navigate: () => {}
})
page.setInput('hello memo')
const result = await page.submit()
assert.equal(result.ok, false)
assert.match(page.broadcastError, /Insufficient balance/)
})