commit fa2569572700d2430dc55f4ff3459fe5f58327a0
parent 78412ac9835332fe05f08638391aa618ce96a815
Author: triesap <tyson@radroots.org>
Date: Tue, 26 May 2026 11:25:06 +0000
cli: fail closed basket quote readiness
Diffstat:
3 files changed, 427 insertions(+), 24 deletions(-)
diff --git a/src/operation_basket.rs b/src/operation_basket.rs
@@ -109,6 +109,13 @@ struct BasketProductBinState {
verified_primary_bin_id: Option<String>,
}
+#[derive(Debug, Clone)]
+enum BasketProductResolution {
+ Resolved(BasketProductBinState),
+ Unresolved,
+ Ambiguous(usize),
+}
+
pub struct BasketOperationService<'a> {
config: &'a RuntimeConfig,
}
@@ -867,7 +874,11 @@ fn basket_market_issues(
document: &BasketDocument,
) -> Result<Vec<BasketIssue>, OperationAdapterError> {
if !config.local.replica_db_path.exists() {
- return Ok(Vec::new());
+ return Ok(vec![basket_issue(
+ "basket_market_replica_missing",
+ "local.replica_db",
+ "current local replica data is required before quote creation; run `radroots store init` and `radroots market refresh`",
+ )]);
}
let executor = SqliteExecutor::open(&config.local.replica_db_path).map_err(|error| {
OperationAdapterError::Runtime(format!(
@@ -877,8 +888,26 @@ fn basket_market_issues(
})?;
let mut issues = Vec::new();
for item in &document.basket.items {
- let Some(product) = basket_product_bin_state(config, &executor, item)? else {
- continue;
+ let product = match basket_product_bin_state(config, &executor, item)? {
+ BasketProductResolution::Resolved(product) => product,
+ BasketProductResolution::Unresolved => {
+ issues.push(basket_issue(
+ "basket_item_listing_unresolved",
+ basket_item_listing_field(item),
+ "basket item listing is not active in the current local replica; run `radroots market refresh` before quote creation",
+ ));
+ continue;
+ }
+ BasketProductResolution::Ambiguous(count) => {
+ issues.push(basket_issue(
+ "basket_item_listing_ambiguous",
+ basket_item_listing_field(item),
+ format!(
+ "basket item listing matched {count} active local replica rows; choose a unique listing before quote creation"
+ ),
+ ));
+ continue;
+ }
};
let Some(primary_bin_id) = product.primary_bin_id.as_deref().and_then(non_empty_ref) else {
issues.push(basket_issue(
@@ -928,7 +957,7 @@ fn basket_product_bin_state(
config: &RuntimeConfig,
executor: &SqliteExecutor,
item: &BasketItem,
-) -> Result<Option<BasketProductBinState>, OperationAdapterError> {
+) -> Result<BasketProductResolution, OperationAdapterError> {
if let Some(listing_addr) = item.listing_addr.as_deref().and_then(non_empty_ref) {
let product_rows = trade_product::find_many(
executor,
@@ -940,17 +969,19 @@ fn basket_product_bin_state(
OperationAdapterError::Runtime(format!("resolve listing product state: {error:?}"))
})?
.results;
- let [product] = product_rows.as_slice() else {
- return Ok(None);
+ let product = match product_rows.as_slice() {
+ [] => return Ok(BasketProductResolution::Unresolved),
+ [product] => product,
+ rows => return Ok(BasketProductResolution::Ambiguous(rows.len())),
};
- return Ok(Some(BasketProductBinState {
+ return Ok(BasketProductResolution::Resolved(BasketProductBinState {
primary_bin_id: product.primary_bin_id.clone(),
verified_primary_bin_id: product.verified_primary_bin_id.clone(),
}));
}
let Some(listing_lookup) = item.listing.as_deref().and_then(non_empty_ref) else {
- return Ok(None);
+ return Ok(BasketProductResolution::Unresolved);
};
let lookup_executor = SqliteExecutor::open(&config.local.replica_db_path).map_err(|error| {
OperationAdapterError::Runtime(format!(
@@ -963,15 +994,30 @@ fn basket_product_bin_state(
.map_err(|error| {
OperationAdapterError::Runtime(format!("resolve listing product state: {error:?}"))
})?;
- let [product] = rows.as_slice() else {
- return Ok(None);
+ let product = match rows.as_slice() {
+ [] => return Ok(BasketProductResolution::Unresolved),
+ [product] => product,
+ rows => return Ok(BasketProductResolution::Ambiguous(rows.len())),
};
- Ok(Some(BasketProductBinState {
+ Ok(BasketProductResolution::Resolved(BasketProductBinState {
primary_bin_id: product.primary_bin_id.clone(),
verified_primary_bin_id: product.verified_primary_bin_id.clone(),
}))
}
+fn basket_item_listing_field(item: &BasketItem) -> String {
+ if item
+ .listing_addr
+ .as_deref()
+ .and_then(non_empty_ref)
+ .is_some()
+ {
+ format!("basket.items.{}.listing_addr", item.item_id)
+ } else {
+ format!("basket.items.{}.listing", item.item_id)
+ }
+}
+
fn basket_issue(
code: impl Into<String>,
field: impl Into<String>,
@@ -1268,9 +1314,14 @@ fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
mod tests {
use std::path::{Path, PathBuf};
+ use radroots_events::RadrootsNostrEvent;
+ use radroots_events::kinds::{KIND_FARM, KIND_LISTING};
+ use radroots_events_codec::trade::RadrootsTradeListingAddress;
+ use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
use radroots_runtime_paths::RadrootsMigrationReport;
use radroots_secret_vault::RadrootsSecretBackend;
- use serde_json::{Map, Value};
+ use radroots_sql_core::{SqlExecutor, SqliteExecutor};
+ use serde_json::{Map, Value, json};
use tempfile::tempdir;
use super::BasketOperationService;
@@ -1386,7 +1437,11 @@ mod tests {
.to_envelope(OperationContext::default().envelope_context("req_basket_validate"))
.expect("basket validate envelope");
assert_eq!(validate_envelope.operation_id, "basket.validate");
- assert_eq!(validate_envelope.result["ready_for_quote"], true);
+ assert_eq!(validate_envelope.result["ready_for_quote"], false);
+ assert_eq!(
+ validate_envelope.result["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
let adjustment_add = OperationRequest::new(
OperationContext::default(),
@@ -1452,6 +1507,7 @@ mod tests {
fn basket_quote_create_materializes_order_draft() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
+ seed_current_listing(&config);
accounts::create_or_migrate_default_account(&config).expect("create buyer account");
let service = OperationAdapter::new(BasketOperationService::new(&config));
create_basket(&service, "basket_quote");
@@ -1505,6 +1561,7 @@ mod tests {
fn basket_quote_create_dry_run_skips_order_draft() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
+ seed_current_listing(&config);
accounts::create_or_migrate_default_account(&config).expect("create buyer account");
let service = OperationAdapter::new(BasketOperationService::new(&config));
create_basket(&service, "basket_dry_run");
@@ -1538,6 +1595,7 @@ mod tests {
fn basket_quote_create_requires_resolved_buyer_account() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
+ seed_current_listing(&config);
let service = OperationAdapter::new(BasketOperationService::new(&config));
create_basket(&service, "basket_no_buyer");
add_listing_item(&service, "basket_no_buyer");
@@ -1556,6 +1614,130 @@ mod tests {
assert_eq!(detail["actions"][0], "radroots account create");
}
+ #[test]
+ fn basket_readiness_fails_closed_without_replica_data() {
+ let dir = tempdir().expect("tempdir");
+ let config = sample_config(dir.path());
+ let service = OperationAdapter::new(BasketOperationService::new(&config));
+ create_basket(&service, "basket_missing_replica");
+ let add = add_listing_item(&service, "basket_missing_replica");
+ assert_eq!(add.result["ready_for_quote"], false);
+ assert_eq!(
+ add.result["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
+
+ let list = OperationRequest::new(OperationContext::default(), BasketListRequest::default())
+ .expect("basket list request");
+ let list_envelope = service
+ .execute(list)
+ .expect("basket list result")
+ .to_envelope(OperationContext::default().envelope_context("req_basket_list"))
+ .expect("basket list envelope");
+ assert_eq!(
+ list_envelope.result["baskets"][0]["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
+
+ let validate = OperationRequest::new(
+ OperationContext::default(),
+ BasketValidateRequest::from_data(data(&[("basket_id", "basket_missing_replica")])),
+ )
+ .expect("basket validate request");
+ let validate_envelope = service
+ .execute(validate)
+ .expect("basket validate result")
+ .to_envelope(OperationContext::default().envelope_context("req_basket_validate"))
+ .expect("basket validate envelope");
+ assert_eq!(validate_envelope.result["state"], "unconfigured");
+ assert_eq!(
+ validate_envelope.result["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
+
+ let quote = OperationRequest::new(
+ OperationContext::default(),
+ BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_missing_replica")])),
+ )
+ .expect("basket quote request");
+ let quote_envelope = service
+ .execute(quote)
+ .expect("basket quote result")
+ .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
+ .expect("basket quote envelope");
+ assert_eq!(quote_envelope.result["state"], "unconfigured");
+ assert_eq!(
+ quote_envelope.result["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
+ assert!(!config.paths.app_data_root.join("orders/drafts").exists());
+ }
+
+ #[test]
+ fn basket_readiness_fails_closed_for_unresolved_listing() {
+ let dir = tempdir().expect("tempdir");
+ let config = sample_config(dir.path());
+ crate::runtime::local::init(&config).expect("store init");
+ let service = OperationAdapter::new(BasketOperationService::new(&config));
+ create_basket(&service, "basket_unresolved");
+ let add = add_listing_item(&service, "basket_unresolved");
+ assert_eq!(add.result["ready_for_quote"], false);
+ assert_eq!(
+ add.result["issues"][0]["code"],
+ "basket_item_listing_unresolved"
+ );
+
+ let quote = OperationRequest::new(
+ OperationContext::default(),
+ BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_unresolved")])),
+ )
+ .expect("basket quote request");
+ let quote_envelope = service
+ .execute(quote)
+ .expect("basket quote result")
+ .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
+ .expect("basket quote envelope");
+ assert_eq!(quote_envelope.result["state"], "unconfigured");
+ assert_eq!(
+ quote_envelope.result["issues"][0]["code"],
+ "basket_item_listing_unresolved"
+ );
+ assert!(!config.paths.app_data_root.join("orders/drafts").exists());
+ }
+
+ #[test]
+ fn basket_readiness_fails_closed_for_ambiguous_listing() {
+ let dir = tempdir().expect("tempdir");
+ let config = sample_config(dir.path());
+ seed_current_listing(&config);
+ duplicate_current_listing_row(&config);
+ let service = OperationAdapter::new(BasketOperationService::new(&config));
+ create_basket(&service, "basket_ambiguous");
+ let add = add_listing_item(&service, "basket_ambiguous");
+ assert_eq!(add.result["ready_for_quote"], false);
+ assert_eq!(
+ add.result["issues"][0]["code"],
+ "basket_item_listing_ambiguous"
+ );
+
+ let quote = OperationRequest::new(
+ OperationContext::default(),
+ BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_ambiguous")])),
+ )
+ .expect("basket quote request");
+ let quote_envelope = service
+ .execute(quote)
+ .expect("basket quote result")
+ .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
+ .expect("basket quote envelope");
+ assert_eq!(quote_envelope.result["state"], "unconfigured");
+ assert_eq!(
+ quote_envelope.result["issues"][0]["code"],
+ "basket_item_listing_ambiguous"
+ );
+ assert!(!config.paths.app_data_root.join("orders/drafts").exists());
+ }
+
fn create_basket(service: &OperationAdapter<BasketOperationService<'_>>, basket_id: &str) {
let request = OperationRequest::new(
OperationContext::default(),
@@ -1565,7 +1747,10 @@ mod tests {
service.execute(request).expect("basket create result");
}
- fn add_listing_item(service: &OperationAdapter<BasketOperationService<'_>>, basket_id: &str) {
+ fn add_listing_item(
+ service: &OperationAdapter<BasketOperationService<'_>>,
+ basket_id: &str,
+ ) -> crate::output_contract::OutputEnvelope {
let request = OperationRequest::new(
OperationContext::default(),
BasketItemAddRequest::from_data(data(&[
@@ -1576,7 +1761,81 @@ mod tests {
])),
)
.expect("basket item add request");
- service.execute(request).expect("basket item add result");
+ service
+ .execute(request)
+ .expect("basket item add result")
+ .to_envelope(OperationContext::default().envelope_context("req_basket_add"))
+ .expect("basket item add envelope")
+ }
+
+ fn seed_current_listing(config: &RuntimeConfig) {
+ crate::runtime::local::init(config).expect("store init");
+ let parsed = RadrootsTradeListingAddress::parse(LISTING_ADDR).expect("listing addr");
+ let event = RadrootsNostrEvent {
+ id: "2".repeat(64),
+ author: parsed.seller_pubkey.clone(),
+ created_at: 1,
+ kind: KIND_LISTING,
+ tags: vec![
+ vec!["d".to_owned(), parsed.listing_id],
+ vec![
+ "a".to_owned(),
+ format!(
+ "{}:{}:{}",
+ KIND_FARM, parsed.seller_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"
+ ),
+ ],
+ vec!["p".to_owned(), parsed.seller_pubkey],
+ vec!["key".to_owned(), "pasture-eggs".to_owned()],
+ vec!["title".to_owned(), "Market Eggs".to_owned()],
+ vec!["category".to_owned(), "eggs".to_owned()],
+ vec!["summary".to_owned(), "Pasture-raised eggs".to_owned()],
+ vec!["process".to_owned(), "washed".to_owned()],
+ vec!["lot".to_owned(), "lot-a".to_owned()],
+ vec!["profile".to_owned(), "dozen".to_owned()],
+ vec!["year".to_owned(), "2026".to_owned()],
+ vec!["radroots:primary_bin".to_owned(), "bin-1".to_owned()],
+ vec![
+ "radroots:bin".to_owned(),
+ "bin-1".to_owned(),
+ "12".to_owned(),
+ "each".to_owned(),
+ "12".to_owned(),
+ "each".to_owned(),
+ "dozen".to_owned(),
+ ],
+ vec![
+ "radroots:price".to_owned(),
+ "bin-1".to_owned(),
+ "6".to_owned(),
+ "USD".to_owned(),
+ "1".to_owned(),
+ "each".to_owned(),
+ "6".to_owned(),
+ "each".to_owned(),
+ ],
+ vec!["inventory".to_owned(), "5".to_owned()],
+ vec!["status".to_owned(), "active".to_owned()],
+ ],
+ content: "# Market Eggs".to_owned(),
+ sig: "f".repeat(128),
+ };
+ let executor = SqliteExecutor::open(&config.local.replica_db_path).expect("open replica");
+ assert_eq!(
+ radroots_replica_ingest_event(&executor, &event).expect("ingest listing"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+ }
+
+ fn duplicate_current_listing_row(config: &RuntimeConfig) {
+ let executor = SqliteExecutor::open(&config.local.replica_db_path).expect("open replica");
+ let params = json!(["33333333-3333-3333-3333-333333333333", LISTING_ADDR]).to_string();
+ executor
+ .exec(
+ "INSERT INTO trade_product (id, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id) SELECT ?, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id FROM trade_product WHERE listing_addr = ?;",
+ params.as_str(),
+ )
+ .expect("duplicate listing row");
}
fn sample_config(root: &Path) -> RuntimeConfig {
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -391,6 +391,21 @@ pub fn update_orderable_listing_primary_bin_id(
.expect("update listing primary bin");
}
+pub fn duplicate_orderable_listing_row(sandbox: &RadrootsCliSandbox, listing_addr: &str) {
+ let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
+ let params = serde_json::to_string(&json!([
+ "33333333-3333-3333-3333-333333333333",
+ listing_addr
+ ]))
+ .expect("duplicate listing params");
+ executor
+ .exec(
+ "INSERT INTO trade_product (id, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id) SELECT ?, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id FROM trade_product WHERE listing_addr = ?;",
+ params.as_str(),
+ )
+ .expect("duplicate listing row");
+}
+
pub fn replace_latest_listing_event_id(
sandbox: &RadrootsCliSandbox,
listing_addr: &str,
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -32,9 +32,9 @@ use serde_json::json;
use support::{
ORDERABLE_LISTING_RELAY, RadrootsCliSandbox, assert_contains,
assert_no_daemon_runtime_reference, assert_no_removed_command_reference, create_listing_draft,
- identity_public, identity_secret, json_from_stdout, make_listing_publishable,
- make_listing_publishable_with_seller, ndjson_from_stdout, radroots, remove_orderable_listing,
- replace_latest_listing_event_id, seed_orderable_listing, toml_string,
+ duplicate_orderable_listing_row, identity_public, identity_secret, json_from_stdout,
+ make_listing_publishable, make_listing_publishable_with_seller, ndjson_from_stdout, radroots,
+ remove_orderable_listing, replace_latest_listing_event_id, seed_orderable_listing, toml_string,
update_orderable_listing_available_amount, update_orderable_listing_primary_bin_id,
write_public_identity_profile, write_secret_identity_profile,
};
@@ -5609,13 +5609,13 @@ fn buyer_market_sync_basket_dry_runs_preflight_without_mutating_local_state() {
"create",
"basket_probe",
]);
- let order_file = quote["result"]["order"]["file"]
- .as_str()
- .expect("order file");
assert_eq!(quote["operation_id"], "basket.quote.create");
- assert_eq!(quote["result"]["state"], "dry_run");
- assert_eq!(quote["result"]["order"]["state"], "dry_run");
- assert!(!Path::new(order_file).exists());
+ assert_eq!(quote["result"]["state"], "unconfigured");
+ assert_eq!(quote["result"]["ready_for_quote"], false);
+ assert_eq!(
+ quote["result"]["issues"][0]["code"],
+ "basket_item_listing_unresolved"
+ );
let basket_after_quote =
sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
@@ -7229,6 +7229,135 @@ fn order_submit_rejects_unknown_local_listing_bin_before_publish() {
}
#[test]
+fn basket_quote_rejects_missing_replica_before_order_write() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+ sandbox.json_success(&["--format", "json", "basket", "create", "missing_replica"]);
+ let add = sandbox.json_success(&[
+ "--format",
+ "json",
+ "basket",
+ "item",
+ "add",
+ "missing_replica",
+ "--listing-addr",
+ LISTING_ADDR,
+ "--bin-id",
+ "bin-1",
+ "--quantity",
+ "2",
+ ]);
+ assert_eq!(add["result"]["ready_for_quote"], false);
+ assert_eq!(
+ add["result"]["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
+
+ let quote = sandbox.json_success(&[
+ "--format",
+ "json",
+ "basket",
+ "quote",
+ "create",
+ "missing_replica",
+ ]);
+ assert_eq!(quote["result"]["state"], "unconfigured");
+ assert_eq!(quote["result"]["ready_for_quote"], false);
+ assert_eq!(
+ quote["result"]["issues"][0]["code"],
+ "basket_market_replica_missing"
+ );
+ assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
+}
+
+#[test]
+fn basket_quote_rejects_unresolved_listing_before_order_write() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+ sandbox.json_success(&["--format", "json", "store", "init"]);
+ sandbox.json_success(&["--format", "json", "basket", "create", "unresolved_listing"]);
+ let add = sandbox.json_success(&[
+ "--format",
+ "json",
+ "basket",
+ "item",
+ "add",
+ "unresolved_listing",
+ "--listing-addr",
+ LISTING_ADDR,
+ "--bin-id",
+ "bin-1",
+ "--quantity",
+ "2",
+ ]);
+ assert_eq!(add["result"]["ready_for_quote"], false);
+ assert_eq!(
+ add["result"]["issues"][0]["code"],
+ "basket_item_listing_unresolved"
+ );
+
+ let quote = sandbox.json_success(&[
+ "--format",
+ "json",
+ "basket",
+ "quote",
+ "create",
+ "unresolved_listing",
+ ]);
+ assert_eq!(quote["result"]["state"], "unconfigured");
+ assert_eq!(quote["result"]["ready_for_quote"], false);
+ assert_eq!(
+ quote["result"]["issues"][0]["code"],
+ "basket_item_listing_unresolved"
+ );
+ assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
+}
+
+#[test]
+fn basket_quote_rejects_ambiguous_listing_before_order_write() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+ seed_orderable_listing(&sandbox, LISTING_ADDR);
+ duplicate_orderable_listing_row(&sandbox, LISTING_ADDR);
+ sandbox.json_success(&["--format", "json", "basket", "create", "ambiguous_listing"]);
+ let add = sandbox.json_success(&[
+ "--format",
+ "json",
+ "basket",
+ "item",
+ "add",
+ "ambiguous_listing",
+ "--listing-addr",
+ LISTING_ADDR,
+ "--bin-id",
+ "bin-1",
+ "--quantity",
+ "2",
+ ]);
+ assert_eq!(add["result"]["ready_for_quote"], false);
+ assert_eq!(
+ add["result"]["issues"][0]["code"],
+ "basket_item_listing_ambiguous"
+ );
+
+ let quote = sandbox.json_success(&[
+ "--format",
+ "json",
+ "basket",
+ "quote",
+ "create",
+ "ambiguous_listing",
+ ]);
+ assert_eq!(quote["result"]["state"], "unconfigured");
+ assert_eq!(quote["result"]["ready_for_quote"], false);
+ assert_eq!(
+ quote["result"]["issues"][0]["code"],
+ "basket_item_listing_ambiguous"
+ );
+ assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
+}
+
+#[test]
fn basket_quote_rejects_invalid_verified_primary_bin_before_order_write() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);