Skip to content

Commit 2eaa18a

Browse files
sgbettclaude
andcommitted
test: migrate specs to new status taxonomy + add HLR coverage (#460)
Update all wallet specs to reflect the status changes introduced by HLR #455: broadcaster is now required (no_send: true exempted), successful broadcast sets status to 'unproven', and 'completed' is reserved for actions that have received a merkle proof via internalize_action. Add new specs covering: validate_broadcast_configuration! guard in create_action and sign_action, internalize_action status ('unproven' vs 'completed' based on BUMP presence), and SolidQueueAdapter job-vs-wallet status distinction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 181a27f commit 2eaa18a

File tree

9 files changed

+397
-189
lines changed

9 files changed

+397
-189
lines changed

gem/bsv-wallet-postgres/spec/bsv/wallet_postgres/solid_queue_adapter_spec.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,9 @@ def action_status(txid)
259259
expect(output_state('chg:0').to_s).to eq('spendable')
260260
end
261261

262-
it 'updates action status to completed' do
263-
expect(action_status(txid_a)).to eq('completed')
262+
# Post-HLR #455: 'unproven' until a merkle proof lands via internalize_action
263+
it 'updates action status to unproven' do
264+
expect(action_status(txid_a)).to eq('unproven')
264265
end
265266
end
266267

@@ -329,8 +330,9 @@ def action_status(txid)
329330
adapter.drain
330331
end
331332

332-
it 'updates action status to completed' do
333-
expect(action_status(txid_a)).to eq('completed')
333+
# Post-HLR #455: 'unproven' until a merkle proof lands via internalize_action
334+
it 'updates action status to unproven' do
335+
expect(action_status(txid_a)).to eq('unproven')
334336
end
335337

336338
it 'marks job as completed' do

gem/bsv-wallet/spec/bsv/wallet_interface/auto_funding_spec.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,19 @@ def p2pkh_hex
6666

6767
let(:private_key) { BSV::Primitives::PrivateKey.generate }
6868
let(:storage) { BSV::Wallet::MemoryStore.new }
69-
let(:wallet) { BSV::Wallet::WalletClient.new(private_key, storage: storage) }
69+
# Post-HLR #455: broadcaster required; create_action raises when absent and no_send unset.
70+
let(:broadcaster) { double('broadcaster') } # rubocop:disable RSpec/VerifiedDoubles
71+
let(:wallet) do
72+
BSV::Wallet::WalletClient.new(private_key, storage: storage, broadcaster: broadcaster)
73+
end
74+
75+
# Stub broadcast for all tests — auto_funding_spec tests auto-fund plumbing,
76+
# not broadcast behaviour. SEEN_ON_NETWORK is the standard success response.
77+
before do
78+
allow(broadcaster).to receive(:broadcast).and_return(
79+
BSV::Network::BroadcastResponse.new(txid: 'stub', tx_status: 'SEEN_ON_NETWORK')
80+
)
81+
end
7082

7183
# ---------------------------------------------------------------------------
7284
# 1. Happy path — simple spend
@@ -130,11 +142,12 @@ def p2pkh_hex
130142
expect(change[:basket]).to eq('default')
131143
end
132144

133-
it 'stores the action as completed' do
145+
# Post-HLR #455: 'unproven' until a merkle proof lands via internalize_action
146+
it 'stores the action as unproven' do
134147
result
135148
actions = storage.find_actions({})
136149
expect(actions).not_to be_empty
137-
expect(actions.last[:status]).to eq('completed')
150+
expect(actions.last[:status]).to eq('unproven')
138151
end
139152

140153
it 'produces a valid BEEF' do
@@ -418,6 +431,7 @@ def p2pkh_hex
418431
bad_wallet = BSV::Wallet::WalletClient.new(
419432
private_key,
420433
storage: storage,
434+
broadcaster: broadcaster,
421435
change_generator: bad_generator
422436
)
423437

@@ -656,6 +670,7 @@ def p2pkh_hex
656670
custom_wallet = BSV::Wallet::WalletClient.new(
657671
private_key,
658672
storage: storage,
673+
broadcaster: broadcaster,
659674
fee_estimator: fee_estimator,
660675
coin_selector: coin_selector,
661676
change_generator: change_generator

gem/bsv-wallet/spec/bsv/wallet_interface/broadcast_rollback_spec.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,12 @@ def recipient_lock_hex
130130
change.each { |o| expect(o[:state]).to eq(:spendable) }
131131
end
132132

133-
it 'stores the action with status "completed"' do
133+
# Post-HLR #455: 'unproven' until a merkle proof lands via internalize_action
134+
it 'stores the action with status "unproven"' do
134135
txid = result[:txid]
135136
actions = storage.find_actions({ limit: 100, offset: 0 })
136137
action = actions.find { |a| a[:txid] == txid }
137-
expect(action[:status]).to eq('completed')
138+
expect(action[:status]).to eq('unproven')
138139
end
139140
end
140141

gem/bsv-wallet/spec/bsv/wallet_interface/inline_queue_spec.rb

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,12 @@ def build_payload(txid:, input_outpoints:, change_outpoints:, fund_ref: 'ref-001
115115
expect(outpoints).to include('xyz:0')
116116
end
117117

118-
it 'updates action status to "completed"' do
118+
# Post-HLR #455: 'unproven' until a merkle proof lands via internalize_action
119+
it 'updates action status to "unproven"' do
119120
result
120121
actions = storage.find_actions({ limit: 10, offset: 0 })
121122
action = actions.find { |a| a[:txid] == txid }
122-
expect(action[:status]).to eq('completed')
123+
expect(action[:status]).to eq('unproven')
123124
end
124125
end
125126

@@ -181,9 +182,9 @@ def build_payload(txid:, input_outpoints:, change_outpoints:, fund_ref: 'ref-001
181182
end
182183

183184
# --------------------------------------------------------------------------
184-
# 3. Without broadcaster — promotes immediately
185+
# 3. Without broadcaster — raises WalletError (HLR #455: no silent fallback)
185186
# --------------------------------------------------------------------------
186-
describe 'without broadcaster' do
187+
describe 'without broadcaster, no accept_delayed_broadcast' do
187188
let(:queue) { described_class.new(storage: storage) }
188189
let(:payload) do
189190
build_payload(
@@ -193,48 +194,21 @@ def build_payload(txid:, input_outpoints:, change_outpoints:, fund_ref: 'ref-001
193194
fund_ref: 'ref-nb'
194195
)
195196
end
196-
let(:result) { queue.enqueue(payload) }
197-
let(:txid) { 'c' * 64 }
198-
let(:input1) { seed_pending_output(storage, outpoint: 'in2:0', fund_ref: 'ref-nb') }
199-
let(:change1) { seed_change_output(storage, outpoint: 'ch2:0') }
197+
let(:txid) { 'c' * 64 }
200198

201199
before do
202-
input1
203-
change1
200+
seed_pending_output(storage, outpoint: 'in2:0', fund_ref: 'ref-nb')
201+
seed_change_output(storage, outpoint: 'ch2:0')
204202
seed_action(storage, txid: txid)
205203
end
206204

207-
it 'returns the txid' do
208-
expect(result[:txid]).to eq(txid)
209-
end
210-
211-
it 'returns tx bytes' do
212-
expect(result[:tx]).to be_an(Array)
213-
end
214-
215-
it 'does not return broadcast_status' do
216-
expect(result).not_to have_key(:broadcast_status)
217-
end
218-
219-
it 'promotes input outputs to :spent' do
220-
result
221-
all = storage.find_outputs({ include_spent: true, limit: 100, offset: 0 })
222-
input = all.find { |o| o[:outpoint] == 'in2:0' }
223-
expect(input[:state]).to eq(:spent)
224-
end
225-
226-
it 'promotes change outputs to :spendable' do
227-
result
228-
spendable = storage.find_spendable_outputs
229-
outpoints = spendable.map { |o| o[:outpoint] }
230-
expect(outpoints).to include('ch2:0')
231-
end
232-
233-
it 'sets action status to "completed"' do
234-
result
235-
actions = storage.find_actions({ limit: 10, offset: 0 })
236-
action = actions.find { |a| a[:txid] == txid }
237-
expect(action[:status]).to eq('completed')
205+
# Post-HLR #455: silent fallback to 'completed' removed; raises WalletError
206+
# unless accept_delayed_broadcast is explicitly set on the payload.
207+
it 'raises WalletError when accept_delayed_broadcast is false' do
208+
expect { queue.enqueue(payload) }.to raise_error(
209+
BSV::Wallet::WalletError,
210+
/accept_delayed_broadcast/
211+
)
238212
end
239213
end
240214

@@ -299,11 +273,12 @@ def build_payload(txid:, input_outpoints:, change_outpoints:, fund_ref: 'ref-001
299273
expect(result[:broadcast_status]).to eq('success')
300274
end
301275

302-
it 'updates action status to "completed"' do
276+
# Post-HLR #455: 'unproven' until a merkle proof lands via internalize_action
277+
it 'updates action status to "unproven"' do
303278
result
304279
actions = storage.find_actions({ limit: 10, offset: 0 })
305280
action = actions.find { |a| a[:txid] == txid }
306-
expect(action[:status]).to eq('completed')
281+
expect(action[:status]).to eq('unproven')
307282
end
308283

309284
it 'does not add any extra outputs' do

gem/bsv-wallet/spec/bsv/wallet_interface/pending_lock_spec.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,17 @@ def seed_output(outpoint: 'tx0:0', satoshis: 1000, basket: 'default')
121121
describe 'abort_action releases pending UTXOs' do
122122
let(:private_key) { BSV::Primitives::PrivateKey.generate }
123123
let(:storage) { BSV::Wallet::MemoryStore.new }
124-
let(:wallet) { BSV::Wallet::WalletClient.new(private_key, storage: storage) }
124+
# Post-HLR #455: broadcaster required; create_action raises without one.
125+
let(:broadcaster) { double('broadcaster') } # rubocop:disable RSpec/VerifiedDoubles
126+
let(:wallet) do
127+
BSV::Wallet::WalletClient.new(private_key, storage: storage, broadcaster: broadcaster)
128+
end
129+
130+
before do
131+
allow(broadcaster).to receive(:broadcast).and_return(
132+
BSV::Network::BroadcastResponse.new(txid: 'stub', tx_status: 'SEEN_ON_NETWORK')
133+
)
134+
end
125135

126136
def seed_wallet_utxo(satoshis:)
127137
prefix = SecureRandom.hex(16)

gem/bsv-wallet/spec/bsv/wallet_interface/proof_round_trip_spec.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55

66
RSpec.describe 'Proof round-trip: internalize_action → create_action → valid BEEF' do
77
let(:private_key) { BSV::Primitives::PrivateKey.generate }
8-
let(:pub_key) { private_key.public_key }
9-
let(:storage) { BSV::Wallet::MemoryStore.new }
10-
let(:wallet) { BSV::Wallet::WalletClient.new(private_key, storage: storage) }
11-
128
# Build a minimal source transaction (coin we're receiving).
139
let(:source_tx) do
1410
tx = BSV::Transaction::Transaction.new
@@ -20,7 +16,6 @@
2016
)
2117
tx
2218
end
23-
2419
# Build a simple merkle proof for the source transaction.
2520
# Level 0 leaf hashes are in internal byte order (reverse of display
2621
# order), matching the BRC-74 wire format and compute_root lookup.
@@ -35,7 +30,6 @@
3530
)
3631
BSV::Transaction::MerklePath.new(block_height: 800_000, path: [[tx_elem, sibling_elem]])
3732
end
38-
3933
# Attach the proof to the source tx, then build a BEEF that carries it.
4034
let(:beef_for_internalize) do
4135
source_tx.merkle_path = merkle_path
@@ -71,13 +65,11 @@
7165

7266
beef.to_binary
7367
end
74-
7568
let(:subject_txid) do
7669
# Parse the BEEF to retrieve the subject transaction's txid.
7770
beef = BSV::Transaction::Beef.from_binary(beef_for_internalize)
7871
beef.transactions.last.transaction.txid_hex
7972
end
80-
8173
let(:internalize_args) do
8274
{
8375
tx: beef_for_internalize.unpack('C*'),
@@ -91,6 +83,17 @@
9183
]
9284
}
9385
end
86+
let(:pub_key) { private_key.public_key }
87+
let(:storage) { BSV::Wallet::MemoryStore.new }
88+
# Post-HLR #455: broadcaster required; create_action raises without one.
89+
let(:broadcaster) { double('broadcaster') } # rubocop:disable RSpec/VerifiedDoubles
90+
let(:wallet) { BSV::Wallet::WalletClient.new(private_key, storage: storage, broadcaster: broadcaster) }
91+
92+
before do
93+
allow(broadcaster).to receive(:broadcast).and_return(
94+
BSV::Network::BroadcastResponse.new(txid: 'stub', tx_status: 'SEEN_ON_NETWORK')
95+
)
96+
end
9497

9598
describe 'internalize_action' do
9699
it 'stores the merkle proof from the BEEF' do

0 commit comments

Comments
 (0)