Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ winres = "0.1"
[profile.dev.package."*"]
debug = "line-tables-only"

# Argon2id is deliberately memory-hard; unoptimized (opt-level=0) each 64 MiB
# derivation runs for seconds AND holds its memory that whole time, so the
# wallet_backend secret-storage tests both ran slowly (5-23s each) and, under
# whole-suite parallelism, overlapped enough to push the host into swap.
# opt-level=3 shrinks each derivation to tens of ms, which also collapses the
# memory-hold window and the swap pressure. This mirrors platform's own root
# argon2 stanza, which does NOT propagate downstream: Cargo honors [profile.*]
# tables only from the workspace root, so DET must declare its own.
[profile.dev.package.argon2]
opt-level = 3

[profile.test.package.argon2]
opt-level = 3

[lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = ["cfg(tokio_unstable)", "cfg(feature, values(\"testing\", \"bench\", \"mcp\", \"cli\", \"headless\"))"]
Expand Down
27 changes: 21 additions & 6 deletions src/context/wallet_lifecycle/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,17 +1181,26 @@ async fn shielded_receive_address_is_none_before_bind() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cache_shielded_receive_address_publishes_bound_account_zero_address() {
let (ctx, sender, _tmp) = offline_testnet_context();
ctx.ensure_wallet_backend(sender)
.await
.expect("ensure_wallet_backend should succeed offline");

let seed = [0xD7u8; 64];
let wallet = crate::model::wallet::Wallet::new_from_seed(seed, Network::Testnet, None, None)
.expect("build wallet");
let seed_hash = wallet.seed_hash();
// Register BEFORE wiring the backend: with no backend yet,
// `register_wallet_upstream` finds none and skips the fire-and-forget
// `wallet_upstream_registration` subtask. Wiring first would spawn that
// subtask, which then races the explicit `ensure_upstream_registered`
// below — both call `create_wallet_from_seed_bytes`, the loser sees
// `WalletAlreadyExists` then `get_wallet` returns `None` in the insert gap
// → `WalletNotFound` (reliably under CI load). This ordering makes
// `ensure_upstream_registered` the single upstream writer.
ctx.register_wallet(wallet, &seed, WalletOrigin::Fresh)
.expect("register wallet");

ctx.ensure_wallet_backend(sender)
.await
.expect("ensure_wallet_backend should succeed offline");

let backend = ctx.wallet_backend().expect("backend wired");
// Mirror `bootstrap_wallet_addresses_jit`'s ordering: a wallet must be
// registered upstream before its Orchard keys can bind.
Expand Down Expand Up @@ -1231,17 +1240,23 @@ async fn cache_shielded_receive_address_publishes_bound_account_zero_address() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remove_wallet_evicts_shielded_receive_address() {
let (ctx, sender, _tmp) = offline_testnet_context();
ctx.ensure_wallet_backend(sender)
.await
.expect("ensure_wallet_backend should succeed offline");

let seed = [0xE9u8; 64];
let wallet = crate::model::wallet::Wallet::new_from_seed(seed, Network::Testnet, None, None)
.expect("build wallet");
let seed_hash = wallet.seed_hash();
// Register BEFORE wiring the backend so `register_wallet_upstream` skips the
// fire-and-forget `wallet_upstream_registration` subtask; otherwise it races
// the explicit `ensure_upstream_registered` below (both call
// `create_wallet_from_seed_bytes`; the loser hits `WalletAlreadyExists` then
// a `None` `get_wallet` in the insert gap → `WalletNotFound` under CI load).
ctx.register_wallet(wallet, &seed, WalletOrigin::Fresh)
.expect("register wallet");

ctx.ensure_wallet_backend(sender)
.await
.expect("ensure_wallet_backend should succeed offline");

let backend = ctx.wallet_backend().expect("backend wired");
// Mirror `bootstrap_wallet_addresses_jit`'s ordering: a wallet must be
// registered upstream before its Orchard keys can bind.
Expand Down
3 changes: 1 addition & 2 deletions tests/kittest/contract_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ fn mount_context() -> (
.with_animations(false)
});
harness.set_size(egui::vec2(1280.0, 800.0));
harness.run_steps(5);
let ctx = harness.state().current_app_context().clone();
let ctx = crate::support::wait_for_wallet_backend(&mut harness);
(harness, ctx)
}

Expand Down
3 changes: 1 addition & 2 deletions tests/kittest/dashpay_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ fn build_ctx() -> (
.expect("AppState builds")
.with_animations(false)
});
h.run_steps(5);
let ctx = h.state().current_app_context().clone();
let ctx = crate::support::wait_for_wallet_backend(&mut h);
(h, ctx)
}

Expand Down
3 changes: 1 addition & 2 deletions tests/kittest/register_dpns_name_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ fn dpns_registration_defaults_to_app_scoped_identity() {
.expect("AppState builds")
.with_animations(false)
});
harness.run_steps(5);
let app_context = harness.state().current_app_context().clone();
let app_context = crate::support::wait_for_wallet_backend(&mut harness);

let _first = seed_identity_for_dpns(&app_context, 0x11, "DPNS Alpha");
let second = seed_identity_for_dpns(&app_context, 0x22, "DPNS Beta");
Expand Down
39 changes: 37 additions & 2 deletions tests/kittest/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,44 @@ use dash_evo_tool::context::AppContext;
use dash_evo_tool::ui::RootScreenType;
use egui_kittest::Harness;
use std::sync::Arc;
use std::time::{Duration, Instant};

pub use data_dir::with_isolated_data_dir;

/// Upper bound a mount helper waits for the wallet backend to finish wiring.
/// Generous on purpose: the poll runs under whole-suite CPU/swap contention
/// (dozens of parallel tests), where a fixed frame count races the async init
/// and intermittently panics `WalletBackendNotYetWired`.
const WALLET_BACKEND_WIRE_TIMEOUT: Duration = Duration::from_secs(30);

/// Step `harness` until its live `AppContext` has a wired wallet backend, then
/// return that context. Panics if the backend is not wired within
/// [`WALLET_BACKEND_WIRE_TIMEOUT`].
///
/// `AppState::new` spawns wallet-backend wiring as a background tokio task, so a
/// fixed `run_steps(N)` gives no guarantee it has completed. Tests that seed the
/// DB via `insert_local_qualified_identity` (which reaches through the backend's
/// k/v store) must gate on this instead of a fixed step count to close the race
/// deterministically — `wallet_backend().is_ok()` is the exact precondition that
/// seeding needs.
pub fn wait_for_wallet_backend(
harness: &mut Harness<'static, dash_evo_tool::app::AppState>,
) -> Arc<AppContext> {
Comment on lines +30 to +32

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: wait_for_wallet_backend's timeout assert isn't attributed to the calling test

Verified: wait_for_wallet_backend (lines 30-46) has no #[track_caller], so its internal assert! (lines 40-43) always reports its panic location as support.rs rather than the specific test that called it. The failure message itself is still informative (it names the 30s timeout), so this only makes backtrace/location triage slightly less direct — low-value but a one-line, essentially free fix.

Suggested change
pub fn wait_for_wallet_backend(
harness: &mut Harness<'static, dash_evo_tool::app::AppState>,
) -> Arc<AppContext> {
#[track_caller]
pub fn wait_for_wallet_backend(
harness: &mut Harness<'static, dash_evo_tool::app::AppState>,
) -> Arc<AppContext> {

source: ['sonnet5-general']

let deadline = Instant::now() + WALLET_BACKEND_WIRE_TIMEOUT;
loop {
harness.step();
let ctx = harness.state().current_app_context().clone();
if ctx.wallet_backend().is_ok() {
return ctx;
}
assert!(
Instant::now() < deadline,
"wallet backend was not wired within {WALLET_BACKEND_WIRE_TIMEOUT:?}"
);
std::thread::sleep(Duration::from_millis(20));
}
}

/// Mounts the full `AppState` on `root_screen` and steps the frame loop until
/// it settles. Skips the app's first-run welcome screen so the requested root
/// screen renders directly. Owns a private tokio runtime for the duration of
Expand All @@ -32,6 +67,7 @@ pub fn mount_app(root_screen: RootScreenType) -> Harness<'static, dash_evo_tool:
app
});
harness.set_size(egui::vec2(1280.0, 800.0));
wait_for_wallet_backend(&mut harness);
harness.run_steps(10);
harness
}
Expand All @@ -48,8 +84,7 @@ pub fn fresh_app_context() -> (tokio::runtime::Runtime, Arc<AppContext>) {
.expect("Failed to create AppState")
.with_animations(false)
});
bootstrap.run_steps(5);
let app_context = bootstrap.state().current_app_context().clone();
let app_context = wait_for_wallet_backend(&mut bootstrap);
drop(bootstrap);
drop(guard);
(rt, app_context)
Expand Down
3 changes: 1 addition & 2 deletions tests/kittest/tokens_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ fn build_ctx() -> (
.expect("AppState builds")
.with_animations(false)
});
h.run_steps(5);
let ctx = h.state().current_app_context().clone();
let ctx = crate::support::wait_for_wallet_backend(&mut h);
(h, ctx)
}

Expand Down
3 changes: 1 addition & 2 deletions tests/kittest/tools_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ fn build_ctx() -> (
.expect("AppState builds")
.with_animations(false)
});
h.run_steps(5);
let ctx = h.state().current_app_context().clone();
let ctx = crate::support::wait_for_wallet_backend(&mut h);
(h, ctx)
}

Expand Down
Loading