Skip to content

Commit 0be256a

Browse files
lklimekclaude
andcommitted
test(rs-platform-wallet/e2e): address review feedback batch — gate live test, framework polish
Restores `#[ignore]` on `transfer_between_two_platform_addresses` so a stock `cargo test -p platform-wallet --all-features` (the workflow runs heavy nextest with no env wiring) stays green; live runs now opt in via `cargo test -- --ignored`. Adds dedicated `Sdk(String)` / `Spv(String)` variants to `FrameworkError` and routes `SdkBuilder::build` / `TrustedHttpContextProvider` / DAPI parse / SPV storage / mn-list-sync failures through them so the underlying error string survives instead of being swallowed by `NotImplemented(&'static str)`. Plumbs the slot-locked workdir into `spv::start_spv` / `build_client_config` so the deferred SPV runtime tracks the cross-process slot lock instead of sharing `<workdir_base>/spv-data`. Reorders `Registry` mutators (insert / remove / set_status) to persist the JSON snapshot before swapping into `self.state` — a failed write now leaves both memory and disk on the prior state, restoring the module's "persist before returning" contract. README + transfer.rs doc comments updated to reflect the gated default. Addresses thepastaclaw findings on PR #3549: - 03f92b9df0f8 / 0f93a68e9734 / fb5e6b538a41 (BLOCKING — #[ignore] gate) - 5ed6efab6c58 / 06120f3487d4 (Sdk/Spv variants) - 113e838341f5 (slot-locked SPV workdir) - 41049103cb71 (registry persist-before-mutate) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 59cba08 commit 0be256a

7 files changed

Lines changed: 102 additions & 61 deletions

File tree

packages/rs-platform-wallet/tests/e2e/README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,19 @@ stable enough to drive from tests. See [Future Core support](#future-core-suppor
4747
- Network access to Dash testnet DAPI nodes (default) or a local/devnet cluster.
4848
- Rust toolchain (stable, matches workspace `rust-toolchain.toml`).
4949

50-
Tests run by default once `tests/.env` exists with a valid bank mnemonic. They are
51-
NOT marked `#[ignore]`. If `PLATFORM_WALLET_E2E_BANK_MNEMONIC` is unset or the bank
52-
is under-funded the harness panics with an actionable message naming the bank's
53-
primary receive address — the failure is operator-actionable, not silent. CI jobs
54-
that run `cargo test` without setting up the operator env will surface that panic;
55-
gate those jobs at the workflow level (e.g. only run e2e on a dedicated job).
50+
Tests are gated behind `#[ignore]` so a stock `cargo test` (or workspace-wide
51+
invocation) stays green for contributors and CI jobs that lack a funded testnet
52+
bank wallet, live DAPI access, and the operator `.env`. To execute the live suite
53+
once setup is in place, opt in explicitly with `--ignored`:
54+
55+
```bash
56+
cargo test --test e2e -- --ignored --nocapture
57+
```
58+
59+
If `PLATFORM_WALLET_E2E_BANK_MNEMONIC` is unset when an opt-in run starts, the
60+
harness panics with an actionable message naming the bank's primary receive
61+
address — the failure is operator-actionable, not silent. An under-funded bank
62+
wallet panics with the same "top up at &lt;address&gt;" pointer.
5663

5764
---
5865

packages/rs-platform-wallet/tests/e2e/cases/transfer.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
//! Self-transfer of credits between two platform-payment addresses
22
//! owned by the same test wallet.
33
//!
4-
//! Runs by default (no `#[ignore]`). Operator setup lives in
5-
//! `tests/.env` (template: `tests/.env.example`). A missing
6-
//! `PLATFORM_WALLET_E2E_BANK_MNEMONIC` surfaces as a
4+
//! Gated behind `#[ignore]` so a stock `cargo test -p platform-wallet`
5+
//! (or workspace-wide invocation) stays green for contributors and CI
6+
//! jobs that lack a funded testnet bank wallet, live DAPI access, and
7+
//! the operator `.env`. Operator setup lives in `tests/.env`
8+
//! (template: `tests/.env.example`); a missing
9+
//! `PLATFORM_WALLET_E2E_BANK_MNEMONIC` would otherwise surface as a
710
//! [`FrameworkError::Bank`](crate::framework::FrameworkError::Bank)
8-
//! during context init; an under-funded bank wallet panics with the
9-
//! README's "top up at <address>" pointer so operators get an
10-
//! actionable target.
11+
//! during context init, escalated to a panic by `setup().expect(..)`.
1112
//!
1213
//! ```bash
1314
//! cp packages/rs-platform-wallet/tests/.env.example \
1415
//! packages/rs-platform-wallet/tests/.env
1516
//! # edit tests/.env to set PLATFORM_WALLET_E2E_BANK_MNEMONIC
16-
//! cargo test --test e2e -- --nocapture
17+
//! cargo test --test e2e -- --ignored --nocapture
1718
//! ```
1819
1920
use std::collections::BTreeMap;
@@ -60,6 +61,7 @@ const TRANSFER_FLOOR: u64 = 1_000_000;
6061
const STEP_TIMEOUT: Duration = Duration::from_secs(60);
6162

6263
#[tokio_shared_rt::test(shared)]
64+
#[ignore = "requires PLATFORM_WALLET_E2E_BANK_MNEMONIC and live testnet access; run with `cargo test -- --ignored`"]
6365
async fn transfer_between_two_platform_addresses() {
6466
let _ = tracing_subscriber::fmt()
6567
.with_env_filter(

packages/rs-platform-wallet/tests/e2e/framework/harness.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ impl E2eContext {
137137
// // Pass the SDK's live address list so SPV peers stay in
138138
// // lock-step with the DAPI endpoints the SDK is actually
139139
// // talking to (port-swapped to the effective P2P port).
140-
// let spv_runtime = spv::start_spv(&manager, &config, sdk.address_list()).await?;
140+
// let spv_runtime = spv::start_spv(&manager, &config, &workdir, sdk.address_list()).await?;
141141
// spv::wait_for_mn_list_synced(&spv_runtime, SPV_READY_TIMEOUT).await?;
142142
// // `set_context_provider` is `ArcSwap`-backed, safe to
143143
// // call after construction.

packages/rs-platform-wallet/tests/e2e/framework/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ pub enum FrameworkError {
107107
/// [`config`].
108108
#[error("e2e config: {0}")]
109109
Config(String),
110+
111+
/// SDK construction / wiring failure (e.g. `SdkBuilder::build`,
112+
/// `TrustedHttpContextProvider::new`, DAPI address parsing).
113+
/// Carries the upstream error stringified so CI logs and any
114+
/// `Result`-matching caller see the underlying cause.
115+
#[error("e2e sdk: {0}")]
116+
Sdk(String),
117+
118+
/// SPV (`dash-spv`) construction / sync failure. Distinct from
119+
/// [`Self::Sdk`] so SPV-only deferred-runtime issues are easy to
120+
/// filter when the SPV path comes back online (Task #15).
121+
#[error("e2e spv: {0}")]
122+
Spv(String),
110123
}
111124

112125
/// Convenience alias used across the harness.

packages/rs-platform-wallet/tests/e2e/framework/registry.rs

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,38 +97,53 @@ impl PersistentTestWalletRegistry {
9797
&self.path
9898
}
9999

100-
/// Insert (or overwrite) an entry, persisting before returning.
101-
/// Last-write-wins on duplicate: failing the insert would risk
102-
/// leaking the new entry, while a sweep can still recover.
100+
/// Insert (or overwrite) an entry, persisting before mutating
101+
/// the in-memory map: the snapshot is built off the current state,
102+
/// written to disk, and only swapped in once the write succeeds.
103+
/// A failed write therefore leaves both memory and disk on the
104+
/// previous state — preserving the module's "persist before
105+
/// returning" contract under partial failure.
106+
/// Last-write-wins on duplicate.
103107
pub fn insert(&self, hash: WalletSeedHash, entry: RegistryEntry) -> FrameworkResult<()> {
104108
let snapshot = {
105-
let mut guard = self.state.lock();
106-
guard.insert(hash, entry);
107-
guard.clone()
109+
let guard = self.state.lock();
110+
let mut snapshot = guard.clone();
111+
snapshot.insert(hash, entry);
112+
snapshot
108113
};
109-
atomic_write_json(&self.path, &snapshot)
114+
atomic_write_json(&self.path, &snapshot)?;
115+
*self.state.lock() = snapshot;
116+
Ok(())
110117
}
111118

112119
/// Remove an entry. Missing-key is OK — teardown is best-effort.
120+
/// Persists before mutating in-memory state (see [`Self::insert`]).
113121
pub fn remove(&self, hash: &WalletSeedHash) -> FrameworkResult<()> {
114122
let snapshot = {
115-
let mut guard = self.state.lock();
116-
guard.remove(hash);
117-
guard.clone()
123+
let guard = self.state.lock();
124+
let mut snapshot = guard.clone();
125+
snapshot.remove(hash);
126+
snapshot
118127
};
119-
atomic_write_json(&self.path, &snapshot)
128+
atomic_write_json(&self.path, &snapshot)?;
129+
*self.state.lock() = snapshot;
130+
Ok(())
120131
}
121132

122-
/// Update [`EntryStatus`]; no-op if the entry is absent.
133+
/// Update [`EntryStatus`]; no-op if the entry is absent. Persists
134+
/// before mutating in-memory state (see [`Self::insert`]).
123135
pub fn set_status(&self, hash: &WalletSeedHash, status: EntryStatus) -> FrameworkResult<()> {
124136
let snapshot = {
125-
let mut guard = self.state.lock();
126-
if let Some(entry) = guard.get_mut(hash) {
137+
let guard = self.state.lock();
138+
let mut snapshot = guard.clone();
139+
if let Some(entry) = snapshot.get_mut(hash) {
127140
entry.status = status;
128141
}
129-
guard.clone()
142+
snapshot
130143
};
131-
atomic_write_json(&self.path, &snapshot)
144+
atomic_write_json(&self.path, &snapshot)?;
145+
*self.state.lock() = snapshot;
146+
Ok(())
132147
}
133148

134149
/// Snapshot of all entries (Active / Failed). The startup sweep

packages/rs-platform-wallet/tests/e2e/framework/sdk.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub fn build_sdk(config: &Config) -> FrameworkResult<Arc<Sdk>> {
3434
.build()
3535
.map_err(|e| {
3636
tracing::error!(target: "platform_wallet::e2e::sdk", "SdkBuilder::build failed: {e}");
37-
FrameworkError::NotImplemented("sdk::build_sdk — SdkBuilder::build failed (see logs)")
37+
FrameworkError::Sdk(format!("SdkBuilder::build failed: {e}"))
3838
})?;
3939

4040
Ok(Arc::new(sdk))
@@ -70,9 +70,9 @@ fn build_trusted_context_provider(
7070
target: "platform_wallet::e2e::sdk",
7171
"TrustedHttpContextProvider construction failed: {e}"
7272
);
73-
FrameworkError::NotImplemented(
74-
"sdk::build_trusted_context_provider — TrustedHttpContextProvider failed (see logs)",
75-
)
73+
FrameworkError::Sdk(format!(
74+
"TrustedHttpContextProvider construction failed: {e}"
75+
))
7676
})
7777
}
7878

@@ -97,9 +97,10 @@ fn build_sdk_builder(config: &Config, network: Network) -> FrameworkResult<SdkBu
9797
"no DAPI addresses configured for {other:?} — set {} to a comma-separated list of DAPI URLs",
9898
super::config::vars::DAPI_ADDRESSES,
9999
);
100-
Err(FrameworkError::NotImplemented(
101-
"sdk::build_sdk_builder — no DAPI addresses configured (see logs)",
102-
))
100+
Err(FrameworkError::Config(format!(
101+
"no DAPI addresses configured for {other:?} — set {} to a comma-separated list of DAPI URLs",
102+
super::config::vars::DAPI_ADDRESSES,
103+
)))
103104
}
104105
}
105106
}
@@ -115,9 +116,7 @@ where
115116
target: "platform_wallet::e2e::sdk",
116117
"invalid DAPI address {s:?}: {e}"
117118
);
118-
FrameworkError::NotImplemented(
119-
"sdk::parse_addresses — invalid DAPI address (see logs)",
120-
)
119+
FrameworkError::Config(format!("invalid DAPI address {s:?}: {e}"))
121120
})
122121
})
123122
.collect()

packages/rs-platform-wallet/tests/e2e/framework/spv.rs

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//! [`PROGRESS_LOG_INTERVAL`] for debuggability.
1313
1414
use std::net::{IpAddr, SocketAddr};
15+
use std::path::Path;
1516
use std::sync::Arc;
1617
use std::time::{Duration, Instant};
1718

@@ -39,9 +40,12 @@ const PROGRESS_LOG_INTERVAL: Duration = Duration::from_secs(30);
3940

4041
/// Spawn the SPV client backing the harness's
4142
/// [`PlatformWalletManager`]. Storage is anchored under
42-
/// `config.workdir_base.join("spv-data")`. Returns the same handle
43-
/// as [`PlatformWalletManager::spv_arc`]; shut it down via
44-
/// [`SpvRuntime::stop`].
43+
/// `<workdir>/spv-data` where `workdir` is the slot the harness
44+
/// already locked via [`super::workdir::pick_available_workdir`] —
45+
/// concurrent processes get distinct slots and therefore distinct
46+
/// SPV stores, so RocksDB never sees cross-process contention.
47+
/// Returns the same handle as [`PlatformWalletManager::spv_arc`];
48+
/// shut it down via [`SpvRuntime::stop`].
4549
///
4650
/// `address_list` is the SDK's live DAPI address list (typically
4751
/// `sdk.address_list()`). P2P peers are seeded from those same
@@ -51,13 +55,14 @@ const PROGRESS_LOG_INTERVAL: Duration = Duration::from_secs(30);
5155
pub async fn start_spv<P>(
5256
manager: &Arc<PlatformWalletManager<P>>,
5357
config: &Config,
58+
workdir: &Path,
5459
address_list: &AddressList,
5560
) -> FrameworkResult<Arc<SpvRuntime>>
5661
where
5762
P: PlatformWalletPersistence + 'static,
5863
{
5964
let spv = manager.spv_arc();
60-
let client_config = build_client_config(config, address_list)?;
65+
let client_config = build_client_config(config, workdir, address_list)?;
6166

6267
spv.spawn_in_background(client_config);
6368
tracing::info!(
@@ -126,8 +131,8 @@ pub async fn wait_for_mn_list_synced(spv: &SpvRuntime, timeout: Duration) -> Fra
126131
target: "platform_wallet::e2e::spv",
127132
"mn-list sync entered Error state"
128133
);
129-
return Err(FrameworkError::NotImplemented(
130-
"spv::wait_for_mn_list_syncedmn-list entered Error state (see logs)",
134+
return Err(FrameworkError::Spv(
135+
"wait_for_mn_list_synced: mn-list entered Error state".to_string(),
131136
));
132137
}
133138
}
@@ -146,9 +151,9 @@ pub async fn wait_for_mn_list_synced(spv: &SpvRuntime, timeout: Duration) -> Fra
146151
target: "platform_wallet::e2e::spv",
147152
"timed out after {effective_timeout:?} waiting for mn-list sync"
148153
);
149-
return Err(FrameworkError::NotImplemented(
150-
"spv::wait_for_mn_list_syncedtimed out (see logs)",
151-
));
154+
return Err(FrameworkError::Spv(format!(
155+
"wait_for_mn_list_synced: timed out after {effective_timeout:?}"
156+
)));
152157
}
153158

154159
tokio::time::sleep(READINESS_POLL_INTERVAL).await;
@@ -202,27 +207,29 @@ fn log_pipeline_snapshot(
202207
}
203208

204209
/// Build the SPV [`ClientConfig`] for `config.network`. Storage
205-
/// under `<workdir>/spv-data`, full validation, bloom-filter
206-
/// mempool tracking, and DAPI peers (extracted from `address_list`)
207-
/// seeded with the effective P2P port — sticks to the SDK's live
208-
/// endpoints to skip DNS-discovered peers that lack compact-block-filter
209-
/// support.
210+
/// under `<workdir>/spv-data` (the slot-locked dir, NOT
211+
/// `workdir_base`), full validation, bloom-filter mempool tracking,
212+
/// and DAPI peers (extracted from `address_list`) seeded with the
213+
/// effective P2P port — sticks to the SDK's live endpoints to skip
214+
/// DNS-discovered peers that lack compact-block-filter support.
210215
fn build_client_config(
211216
config: &Config,
217+
workdir: &Path,
212218
address_list: &AddressList,
213219
) -> FrameworkResult<ClientConfig> {
214220
let network = config.network;
215221

216-
let storage_path = config.workdir_base.join("spv-data");
222+
let storage_path = workdir.join("spv-data");
217223
std::fs::create_dir_all(&storage_path).map_err(|e| {
218224
tracing::error!(
219225
target: "platform_wallet::e2e::spv",
220226
"failed to create SPV storage dir {}: {e}",
221227
storage_path.display()
222228
);
223-
FrameworkError::NotImplemented(
224-
"spv::build_client_config — failed to create SPV storage dir (see logs)",
225-
)
229+
FrameworkError::Spv(format!(
230+
"failed to create SPV storage dir {}: {e}",
231+
storage_path.display()
232+
))
226233
})?;
227234

228235
let mut client_config = ClientConfig::new(network)
@@ -238,9 +245,7 @@ fn build_client_config(
238245
target: "platform_wallet::e2e::spv",
239246
"invalid SPV ClientConfig: {e}"
240247
);
241-
FrameworkError::NotImplemented(
242-
"spv::build_client_config — invalid SPV ClientConfig (see logs)",
243-
)
248+
FrameworkError::Spv(format!("invalid SPV ClientConfig: {e}"))
244249
})?;
245250

246251
Ok(client_config)

0 commit comments

Comments
 (0)