cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

target_cli.rs (269510B)


      1 mod support;
      2 
      3 use std::fs;
      4 use std::io::{Read, Write};
      5 use std::net::{TcpListener, TcpStream};
      6 use std::path::{Path, PathBuf};
      7 use std::sync::{
      8     Arc, Mutex,
      9     atomic::{AtomicBool, Ordering},
     10 };
     11 use std::thread::{self, JoinHandle};
     12 use std::time::{Duration, Instant};
     13 
     14 use nostr::nips::nip44::{self, Version};
     15 use nostr::{EventBuilder, Keys, Kind, PublicKey, SecretKey, Tag};
     16 use radroots_events::RadrootsNostrEventPtr;
     17 use radroots_events::ids::{
     18     RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey,
     19 };
     20 use radroots_events::kinds::{KIND_LISTING, KIND_ORDER_REQUEST};
     21 use radroots_events::order::{RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRequest};
     22 use radroots_events_codec::order::order_request_event_build;
     23 use radroots_identity::RadrootsIdentity;
     24 use radroots_local_events::{
     25     BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecordInput, LocalEventsStore,
     26     LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence,
     27     SourceRuntime, canonical_relay_set_fingerprint,
     28 };
     29 use radroots_nostr::prelude::{RadrootsNostrEvent, radroots_nostr_build_event};
     30 use radroots_nostr_connect::prelude::{
     31     RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequest,
     32     RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
     33 };
     34 use radroots_replica_db::{farm, migrations};
     35 use radroots_replica_db_schema::farm::IFarmFields;
     36 use radroots_replica_sync::radroots_replica_pending_publish_batch;
     37 use radroots_sql_core::SqliteExecutor;
     38 use serde_json::Value;
     39 use serde_json::json;
     40 
     41 use support::{
     42     ORDERABLE_LISTING_RELAY, RadrootsCliSandbox, assert_contains,
     43     assert_no_daemon_runtime_reference, assert_no_removed_command_reference, create_listing_draft,
     44     duplicate_orderable_listing_row, identity_public, identity_secret, json_from_stdout,
     45     make_listing_publishable, ndjson_from_stdout, radroots, remove_orderable_listing,
     46     replace_latest_listing_event_id, seed_orderable_listing, store_test_session_secret,
     47     toml_string, update_orderable_listing_available_amount,
     48     update_orderable_listing_primary_bin_id, write_public_identity_profile,
     49     write_secret_identity_profile,
     50 };
     51 
     52 const LISTING_ADDR: &str =
     53     "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
     54 const LEGACY_SYNC_PUSH_FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     55 
     56 fn test_order_id(value: &str) -> RadrootsOrderId {
     57     value.parse().expect("valid order id")
     58 }
     59 
     60 fn test_listing_addr(value: &str) -> RadrootsListingAddress {
     61     value.parse().expect("valid listing address")
     62 }
     63 
     64 fn test_inventory_bin_id(value: &str) -> RadrootsInventoryBinId {
     65     value.parse().expect("valid inventory bin id")
     66 }
     67 
     68 fn test_pubkey(value: &str) -> RadrootsPublicKey {
     69     value.parse().expect("valid public key")
     70 }
     71 
     72 fn radrootsd_proxy_token_file(sandbox: &RadrootsCliSandbox) -> PathBuf {
     73     let path = sandbox.root().join("radrootsd_proxy.token");
     74     fs::write(&path, "proxy_test_token\n").expect("write proxy token file");
     75     path
     76 }
     77 
     78 struct RelayFetchServer {
     79     endpoint: String,
     80     handle: JoinHandle<()>,
     81 }
     82 
     83 impl RelayFetchServer {
     84     fn with_events(events: Vec<RadrootsNostrEvent>) -> Self {
     85         let listener = TcpListener::bind("127.0.0.1:0").expect("bind relay fetch");
     86         let endpoint = format!("ws://{}", listener.local_addr().expect("relay fetch addr"));
     87         let handle = thread::spawn(move || {
     88             let (stream, _) = listener.accept().expect("accept relay fetch connection");
     89             handle_relay_fetch_connection(stream, events);
     90         });
     91         Self { endpoint, handle }
     92     }
     93 
     94     fn endpoint(&self) -> &str {
     95         self.endpoint.as_str()
     96     }
     97 
     98     fn join(self) {
     99         self.handle.join().expect("relay fetch server join");
    100     }
    101 }
    102 
    103 struct RadrootsdProxyJsonRpcServer {
    104     endpoint: String,
    105     handle: JoinHandle<Value>,
    106 }
    107 
    108 impl RadrootsdProxyJsonRpcServer {
    109     fn once(expected_token: &'static str) -> Self {
    110         let listener = TcpListener::bind("127.0.0.1:0").expect("bind radrootsd proxy");
    111         listener
    112             .set_nonblocking(true)
    113             .expect("radrootsd proxy nonblocking");
    114         let endpoint = format!("http://{}", listener.local_addr().expect("proxy addr"));
    115         let handle = thread::spawn(move || {
    116             let deadline = Instant::now() + Duration::from_secs(10);
    117             loop {
    118                 match listener.accept() {
    119                     Ok((stream, _)) => {
    120                         return handle_radrootsd_proxy_connection(stream, expected_token);
    121                     }
    122                     Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
    123                         assert!(
    124                             Instant::now() < deadline,
    125                             "timed out waiting for radrootsd proxy request"
    126                         );
    127                         thread::sleep(Duration::from_millis(10));
    128                     }
    129                     Err(error) => panic!("accept radrootsd proxy connection: {error}"),
    130                 }
    131             }
    132         });
    133         Self { endpoint, handle }
    134     }
    135 
    136     fn endpoint(&self) -> &str {
    137         self.endpoint.as_str()
    138     }
    139 
    140     fn join(self) -> Value {
    141         self.handle.join().expect("radrootsd proxy server join")
    142     }
    143 }
    144 
    145 #[derive(Clone, Copy)]
    146 enum Nip46RelayFinish {
    147     SignResponse,
    148     ProductPublish,
    149 }
    150 
    151 struct Nip46RelayReport {
    152     connection_count: usize,
    153     req_count: usize,
    154     sign_request_count: usize,
    155     published_events: Vec<RadrootsNostrEvent>,
    156 }
    157 
    158 struct Nip46RelayState {
    159     connection_count: Mutex<usize>,
    160     req_count: Mutex<usize>,
    161     sign_request_count: Mutex<usize>,
    162     published_events: Mutex<Vec<RadrootsNostrEvent>>,
    163     pending_responses: Mutex<Vec<RadrootsNostrEvent>>,
    164     done: AtomicBool,
    165     finish: Nip46RelayFinish,
    166 }
    167 
    168 struct Nip46RelayServer {
    169     endpoint: String,
    170     state: Arc<Nip46RelayState>,
    171     handle: JoinHandle<()>,
    172 }
    173 
    174 impl Nip46RelayServer {
    175     fn new(remote_keys: Keys, user_keys: Keys, finish: Nip46RelayFinish) -> Self {
    176         let listener = TcpListener::bind("127.0.0.1:0").expect("bind nip46 relay");
    177         listener.set_nonblocking(true).expect("nip46 nonblocking");
    178         let endpoint = format!("ws://{}", listener.local_addr().expect("nip46 addr"));
    179         let state = Arc::new(Nip46RelayState {
    180             connection_count: Mutex::new(0),
    181             req_count: Mutex::new(0),
    182             sign_request_count: Mutex::new(0),
    183             published_events: Mutex::new(Vec::new()),
    184             pending_responses: Mutex::new(Vec::new()),
    185             done: AtomicBool::new(false),
    186             finish,
    187         });
    188         let thread_state = Arc::clone(&state);
    189         let remote_keys = Arc::new(remote_keys);
    190         let user_keys = Arc::new(user_keys);
    191         let handle = thread::spawn(move || {
    192             let deadline = Instant::now() + Duration::from_secs(10);
    193             while Instant::now() < deadline && !thread_state.done.load(Ordering::SeqCst) {
    194                 match listener.accept() {
    195                     Ok((stream, _)) => {
    196                         *thread_state
    197                             .connection_count
    198                             .lock()
    199                             .expect("connection count lock") += 1;
    200                         let connection_state = Arc::clone(&thread_state);
    201                         let remote_keys = Arc::clone(&remote_keys);
    202                         let user_keys = Arc::clone(&user_keys);
    203                         thread::spawn(move || {
    204                             handle_nip46_relay_connection(
    205                                 stream,
    206                                 (*remote_keys).clone(),
    207                                 (*user_keys).clone(),
    208                                 connection_state,
    209                             );
    210                         });
    211                     }
    212                     Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
    213                         thread::sleep(Duration::from_millis(10));
    214                     }
    215                     Err(error) => panic!("accept nip46 relay connection: {error}"),
    216                 }
    217             }
    218             assert!(
    219                 thread_state.done.load(Ordering::SeqCst),
    220                 "timed out waiting for NIP-46 relay proof; connections {}; req {}; sign requests {}; published events {}",
    221                 *thread_state
    222                     .connection_count
    223                     .lock()
    224                     .expect("connection count lock"),
    225                 *thread_state.req_count.lock().expect("req count lock"),
    226                 *thread_state
    227                     .sign_request_count
    228                     .lock()
    229                     .expect("sign count lock"),
    230                 thread_state
    231                     .published_events
    232                     .lock()
    233                     .expect("published events lock")
    234                     .len(),
    235             );
    236         });
    237         Self {
    238             endpoint,
    239             state,
    240             handle,
    241         }
    242     }
    243 
    244     fn endpoint(&self) -> &str {
    245         self.endpoint.as_str()
    246     }
    247 
    248     fn join(self) -> Nip46RelayReport {
    249         self.handle.join().expect("nip46 relay server join");
    250         Nip46RelayReport {
    251             connection_count: *self
    252                 .state
    253                 .connection_count
    254                 .lock()
    255                 .expect("connection count lock"),
    256             req_count: *self.state.req_count.lock().expect("req count lock"),
    257             sign_request_count: *self
    258                 .state
    259                 .sign_request_count
    260                 .lock()
    261                 .expect("sign count lock"),
    262             published_events: self
    263                 .state
    264                 .published_events
    265                 .lock()
    266                 .expect("published events lock")
    267                 .clone(),
    268         }
    269     }
    270 }
    271 
    272 fn handle_nip46_relay_connection(
    273     stream: TcpStream,
    274     remote_keys: Keys,
    275     user_keys: Keys,
    276     state: Arc<Nip46RelayState>,
    277 ) {
    278     stream
    279         .set_nonblocking(false)
    280         .expect("nip46 blocking stream");
    281     let mut websocket = tungstenite::accept(stream).expect("accept nip46 websocket");
    282     let mut subscriptions = Vec::<String>::new();
    283     loop {
    284         let message = match websocket.read() {
    285             Ok(message) => message,
    286             Err(_) => return,
    287         };
    288         if !message.is_text() {
    289             continue;
    290         }
    291         let value: Value =
    292             serde_json::from_str(message.to_text().expect("nip46 text")).expect("nip46 json");
    293         match value.get(0).and_then(Value::as_str) {
    294             Some("REQ") => {
    295                 *state.req_count.lock().expect("req count lock") += 1;
    296                 let subscription_id = value
    297                     .get(1)
    298                     .and_then(Value::as_str)
    299                     .expect("nip46 subscription id")
    300                     .to_owned();
    301                 subscriptions.push(subscription_id.clone());
    302                 websocket
    303                     .send(tungstenite::Message::Text(
    304                         json!(["EOSE", subscription_id]).to_string().into(),
    305                     ))
    306                     .expect("send nip46 eose");
    307                 send_pending_nip46_responses(&mut websocket, subscription_id.as_str(), &state);
    308             }
    309             Some("CLOSE") => {}
    310             Some("EVENT") => {
    311                 let event: RadrootsNostrEvent =
    312                     serde_json::from_value(value.get(1).cloned().expect("nip46 relay event"))
    313                         .expect("parse nip46 relay event");
    314                 if event.kind == Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
    315                     handle_nip46_sign_request(
    316                         &mut websocket,
    317                         &subscriptions,
    318                         event,
    319                         &remote_keys,
    320                         &user_keys,
    321                         &state,
    322                     );
    323                 } else {
    324                     handle_nip46_product_publish(&mut websocket, event, &state);
    325                 }
    326             }
    327             _ => {}
    328         }
    329     }
    330 }
    331 
    332 fn handle_nip46_sign_request(
    333     websocket: &mut tungstenite::WebSocket<TcpStream>,
    334     subscriptions: &[String],
    335     event: RadrootsNostrEvent,
    336     remote_keys: &Keys,
    337     user_keys: &Keys,
    338     state: &Nip46RelayState,
    339 ) {
    340     send_relay_ok(websocket, &event);
    341     let decrypted = nip44::decrypt(remote_keys.secret_key(), &event.pubkey, &event.content)
    342         .expect("decrypt nip46 request");
    343     let request: RadrootsNostrConnectRequestMessage =
    344         serde_json::from_str(&decrypted).expect("decode nip46 request");
    345     let response = match request.request {
    346         RadrootsNostrConnectRequest::SignEvent(unsigned_event) => {
    347             let signed = unsigned_event
    348                 .sign_with_keys(user_keys)
    349                 .expect("sign nip46 request");
    350             RadrootsNostrConnectResponse::SignedEvent(signed)
    351         }
    352         other => RadrootsNostrConnectResponse::Error {
    353             result: None,
    354             error: format!("unexpected test NIP-46 method `{}`", other.method()),
    355         },
    356     };
    357     let response_event =
    358         nip46_response_event(remote_keys, event.pubkey, request.id.as_str(), response);
    359     state
    360         .pending_responses
    361         .lock()
    362         .expect("pending response lock")
    363         .push(response_event.clone());
    364     for subscription_id in subscriptions {
    365         send_nip46_response(websocket, subscription_id.as_str(), &response_event);
    366     }
    367     *state
    368         .sign_request_count
    369         .lock()
    370         .expect("sign request count lock") += 1;
    371     if matches!(state.finish, Nip46RelayFinish::SignResponse) {
    372         state.done.store(true, Ordering::SeqCst);
    373     }
    374 }
    375 
    376 fn send_pending_nip46_responses(
    377     websocket: &mut tungstenite::WebSocket<TcpStream>,
    378     subscription_id: &str,
    379     state: &Nip46RelayState,
    380 ) {
    381     let responses = state
    382         .pending_responses
    383         .lock()
    384         .expect("pending response lock")
    385         .clone();
    386     for response in responses {
    387         send_nip46_response(websocket, subscription_id, &response);
    388     }
    389 }
    390 
    391 fn send_nip46_response(
    392     websocket: &mut tungstenite::WebSocket<TcpStream>,
    393     subscription_id: &str,
    394     response_event: &RadrootsNostrEvent,
    395 ) {
    396     websocket
    397         .send(tungstenite::Message::Text(
    398             json!(["EVENT", subscription_id, response_event])
    399                 .to_string()
    400                 .into(),
    401         ))
    402         .expect("send nip46 response event");
    403 }
    404 
    405 fn handle_nip46_product_publish(
    406     websocket: &mut tungstenite::WebSocket<TcpStream>,
    407     event: RadrootsNostrEvent,
    408     state: &Nip46RelayState,
    409 ) {
    410     send_relay_ok(websocket, &event);
    411     state
    412         .published_events
    413         .lock()
    414         .expect("published events lock")
    415         .push(event);
    416     if matches!(state.finish, Nip46RelayFinish::ProductPublish) {
    417         state.done.store(true, Ordering::SeqCst);
    418     }
    419 }
    420 
    421 fn nip46_response_event(
    422     remote_keys: &Keys,
    423     client_public_key: PublicKey,
    424     request_id: &str,
    425     response: RadrootsNostrConnectResponse,
    426 ) -> RadrootsNostrEvent {
    427     let envelope = response
    428         .into_envelope(request_id)
    429         .expect("nip46 response envelope");
    430     let payload = serde_json::to_string(&envelope).expect("nip46 response payload");
    431     let ciphertext = nip44::encrypt(
    432         remote_keys.secret_key(),
    433         &client_public_key,
    434         payload,
    435         Version::V2,
    436     )
    437     .expect("nip46 response ciphertext");
    438     EventBuilder::new(Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), ciphertext)
    439         .tag(Tag::public_key(client_public_key))
    440         .sign_with_keys(remote_keys)
    441         .expect("nip46 response event")
    442 }
    443 
    444 fn send_relay_ok(websocket: &mut tungstenite::WebSocket<TcpStream>, event: &RadrootsNostrEvent) {
    445     websocket
    446         .send(tungstenite::Message::Text(
    447             json!(["OK", event.id.to_hex(), true, ""])
    448                 .to_string()
    449                 .into(),
    450         ))
    451         .expect("send relay ok");
    452 }
    453 
    454 fn nostr_keys_from_identity(identity: &RadrootsIdentity) -> Keys {
    455     let secret_key_hex = identity.secret_key_hex();
    456     Keys::new(SecretKey::from_hex(secret_key_hex.as_str()).expect("secret key"))
    457 }
    458 
    459 fn myc_nip46_config(
    460     remote_signer_pubkey: &str,
    461     relay_endpoint: &str,
    462     managed_account_ref: &str,
    463     session_ref: &str,
    464 ) -> String {
    465     format!(
    466         r#"[signer]
    467 backend = "myc"
    468 
    469 [[capability_binding]]
    470 capability = "signer.remote_nip46"
    471 provider = "myc"
    472 target_kind = "explicit_endpoint"
    473 target = "bunker://{}?relay={}"
    474 managed_account_ref = "{}"
    475 signer_session_ref = "{}"
    476 "#,
    477         toml_string(remote_signer_pubkey),
    478         toml_string(relay_endpoint),
    479         toml_string(managed_account_ref),
    480         toml_string(session_ref),
    481     )
    482 }
    483 
    484 fn handle_radrootsd_proxy_connection(mut stream: TcpStream, expected_token: &str) -> Value {
    485     let mut bytes = Vec::new();
    486     let mut buffer = [0_u8; 1024];
    487     let body_start = loop {
    488         let read = stream.read(&mut buffer).expect("read radrootsd proxy");
    489         assert!(read > 0, "radrootsd proxy request closed before headers");
    490         bytes.extend_from_slice(&buffer[..read]);
    491         if let Some(index) = http_body_start(&bytes) {
    492             break index;
    493         }
    494     };
    495     let headers = String::from_utf8(bytes[..body_start].to_vec()).expect("headers utf8");
    496     let content_length = http_content_length(headers.as_str());
    497     while bytes.len() < body_start + content_length {
    498         let read = stream.read(&mut buffer).expect("read radrootsd proxy body");
    499         assert!(read > 0, "radrootsd proxy request closed before body");
    500         bytes.extend_from_slice(&buffer[..read]);
    501     }
    502     let body = String::from_utf8(bytes[body_start..body_start + content_length].to_vec())
    503         .expect("body utf8");
    504     assert!(
    505         headers
    506             .to_ascii_lowercase()
    507             .contains(format!("authorization: bearer {expected_token}").as_str()),
    508         "radrootsd proxy request missing expected bearer auth: {headers}"
    509     );
    510     let request: Value = serde_json::from_str(body.as_str()).expect("radrootsd proxy json");
    511     assert_eq!(request["jsonrpc"], "2.0");
    512     assert_eq!(request["method"], "publish.event");
    513     let event = &request["params"]["event"];
    514     let relays = request["params"]["relays"]
    515         .as_array()
    516         .cloned()
    517         .unwrap_or_default();
    518     let relay_results = relays
    519         .iter()
    520         .map(|relay| {
    521             json!({
    522                 "relay_url": relay,
    523                 "source": "request",
    524                 "attempted": true,
    525                 "outcome_kind": "accepted"
    526             })
    527         })
    528         .collect::<Vec<_>>();
    529     let response = json!({
    530         "jsonrpc": "2.0",
    531         "id": request["id"],
    532         "result": {
    533             "deduplicated": false,
    534             "job": {
    535                 "job_id": "cli-proxy-job-1",
    536                 "status": "delivery_satisfied",
    537                 "terminal": true,
    538                 "delivery_satisfied": true,
    539                 "event_id": event["id"],
    540                 "pubkey": event["pubkey"],
    541                 "event_kind": event["kind"],
    542                 "relay_policy": request["params"]["relay_policy"],
    543                 "delivery_policy": request["params"]["delivery_policy"],
    544                 "relay_count": relay_results.len(),
    545                 "acknowledged_count": relay_results.len(),
    546                 "retryable_count": 0,
    547                 "terminal_count": 0,
    548                 "requested_at_ms": 1_700_000_000_000_i64,
    549                 "completed_at_ms": 1_700_000_000_001_i64,
    550                 "relays": relay_results
    551             }
    552         }
    553     });
    554     let response_body = response.to_string();
    555     let raw_response = format!(
    556         "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{response_body}",
    557         response_body.len()
    558     );
    559     stream
    560         .write_all(raw_response.as_bytes())
    561         .expect("write radrootsd proxy response");
    562     request
    563 }
    564 
    565 fn http_body_start(bytes: &[u8]) -> Option<usize> {
    566     bytes
    567         .windows(4)
    568         .position(|window| window == b"\r\n\r\n")
    569         .map(|index| index + 4)
    570 }
    571 
    572 fn http_content_length(headers: &str) -> usize {
    573     headers
    574         .lines()
    575         .find_map(|line| {
    576             let (name, value) = line.split_once(':')?;
    577             if name.eq_ignore_ascii_case("content-length") {
    578                 Some(value.trim().parse::<usize>().expect("content length"))
    579             } else {
    580                 None
    581             }
    582         })
    583         .expect("content-length header")
    584 }
    585 
    586 fn handle_relay_fetch_connection(stream: TcpStream, events: Vec<RadrootsNostrEvent>) {
    587     let mut websocket = tungstenite::accept(stream).expect("accept fetch websocket");
    588     let subscription_id = read_relay_req_subscription_id(&mut websocket);
    589     for event in events {
    590         websocket
    591             .send(tungstenite::Message::Text(
    592                 json!(["EVENT", subscription_id, event]).to_string().into(),
    593             ))
    594             .expect("relay event send");
    595     }
    596     websocket
    597         .send(tungstenite::Message::Text(
    598             json!(["EOSE", subscription_id]).to_string().into(),
    599         ))
    600         .expect("relay eose send");
    601 }
    602 
    603 fn read_relay_req_subscription_id(websocket: &mut tungstenite::WebSocket<TcpStream>) -> String {
    604     loop {
    605         let message = websocket.read().expect("relay req message");
    606         if !message.is_text() {
    607             continue;
    608         }
    609         let value: Value =
    610             serde_json::from_str(message.to_text().expect("relay req text")).expect("relay json");
    611         if value.get(0).and_then(Value::as_str) == Some("REQ") {
    612             return value
    613                 .get(1)
    614                 .and_then(Value::as_str)
    615                 .expect("subscription id")
    616                 .to_owned();
    617         }
    618     }
    619 }
    620 
    621 fn seed_legacy_replica_sync_farm(sandbox: &RadrootsCliSandbox, d_tag: &str, pubkey: &str) {
    622     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica");
    623     migrations::run_all_up(&executor).expect("replica migrations");
    624     farm::create(
    625         &executor,
    626         &IFarmFields {
    627             d_tag: d_tag.to_owned(),
    628             pubkey: pubkey.to_owned(),
    629             name: "Sync Push Farm".to_owned(),
    630             about: Some("sync push process fixture".to_owned()),
    631             website: None,
    632             picture: None,
    633             banner: None,
    634             location_primary: None,
    635             location_city: None,
    636             location_region: None,
    637             location_country: None,
    638         },
    639     )
    640     .expect("seed legacy replica sync farm");
    641 }
    642 
    643 fn seed_app_farm_record(
    644     sandbox: &RadrootsCliSandbox,
    645     account_id: &str,
    646     seller_pubkey: &str,
    647     farm_d_tag: &str,
    648 ) {
    649     append_app_local_record(
    650         LocalEventRecordInput {
    651             record_id: format!("app:local_work:farm:{farm_d_tag}:test"),
    652             family: LocalRecordFamily::LocalWork,
    653             status: LocalRecordStatus::LocalSaved,
    654             source_runtime: SourceRuntime::App,
    655             created_at_ms: 1_779_000_001_000,
    656             inserted_at_ms: 1_779_000_001_000,
    657             owner_account_id: Some(account_id.to_owned()),
    658             owner_pubkey: Some(seller_pubkey.to_owned()),
    659             farm_id: Some(farm_d_tag.to_owned()),
    660             listing_addr: None,
    661             local_work_json: Some(json!({
    662                 "record_kind": "farm_config_v1",
    663                 "scope": "app",
    664                 "document": {
    665                     "version": 1,
    666                     "selection": {
    667                         "scope": "app",
    668                         "account": account_id,
    669                         "farm_d_tag": farm_d_tag,
    670                     },
    671                     "profile": {
    672                         "name": "App Farm",
    673                         "display_name": "App Farm",
    674                     },
    675                     "farm": {
    676                         "d_tag": farm_d_tag,
    677                         "name": "App Farm",
    678                         "location": {
    679                             "primary": "farmstand",
    680                         },
    681                     },
    682                     "listing_defaults": {
    683                         "delivery_method": "pickup",
    684                         "location": {
    685                             "primary": "farmstand",
    686                         },
    687                     },
    688                 },
    689             })),
    690             event_id: None,
    691             event_kind: None,
    692             event_pubkey: None,
    693             event_created_at: None,
    694             event_tags_json: None,
    695             event_content: None,
    696             event_sig: None,
    697             raw_event_json: None,
    698             outbox_status: PublishOutboxStatus::None,
    699             relay_set_fingerprint: None,
    700             relay_delivery_json: None,
    701         },
    702         sandbox,
    703     );
    704 }
    705 
    706 fn seed_app_listing_record(
    707     sandbox: &RadrootsCliSandbox,
    708     account_id: &str,
    709     seller_pubkey: &str,
    710     farm_d_tag: &str,
    711     listing_d_tag: &str,
    712 ) -> String {
    713     seed_app_listing_record_variant(
    714         sandbox,
    715         account_id,
    716         Some(seller_pubkey),
    717         farm_d_tag,
    718         listing_d_tag,
    719         "test",
    720         "App Eggs",
    721         None,
    722     )
    723 }
    724 
    725 fn seed_app_listing_record_variant(
    726     sandbox: &RadrootsCliSandbox,
    727     account_id: &str,
    728     seller_pubkey: Option<&str>,
    729     farm_d_tag: &str,
    730     listing_d_tag: &str,
    731     record_suffix: &str,
    732     title: &str,
    733     exportability: Option<serde_json::Value>,
    734 ) -> String {
    735     seed_app_listing_record_variant_with_listing_addr(
    736         sandbox,
    737         account_id,
    738         seller_pubkey,
    739         farm_d_tag,
    740         listing_d_tag,
    741         record_suffix,
    742         title,
    743         exportability,
    744         true,
    745     )
    746 }
    747 
    748 fn seed_app_listing_record_variant_without_listing_addr(
    749     sandbox: &RadrootsCliSandbox,
    750     account_id: &str,
    751     seller_pubkey: Option<&str>,
    752     farm_d_tag: &str,
    753     listing_d_tag: &str,
    754     record_suffix: &str,
    755     title: &str,
    756 ) -> String {
    757     seed_app_listing_record_variant_with_listing_addr(
    758         sandbox,
    759         account_id,
    760         seller_pubkey,
    761         farm_d_tag,
    762         listing_d_tag,
    763         record_suffix,
    764         title,
    765         None,
    766         false,
    767     )
    768 }
    769 
    770 fn seed_app_listing_record_variant_with_listing_addr(
    771     sandbox: &RadrootsCliSandbox,
    772     account_id: &str,
    773     seller_pubkey: Option<&str>,
    774     farm_d_tag: &str,
    775     listing_d_tag: &str,
    776     record_suffix: &str,
    777     title: &str,
    778     exportability: Option<serde_json::Value>,
    779     include_listing_addr: bool,
    780 ) -> String {
    781     seed_app_listing_record_identity_variant(
    782         sandbox,
    783         account_id,
    784         seller_pubkey,
    785         seller_pubkey,
    786         farm_d_tag,
    787         listing_d_tag,
    788         record_suffix,
    789         title,
    790         exportability,
    791         include_listing_addr,
    792     )
    793 }
    794 
    795 fn seed_app_listing_record_identity_variant(
    796     sandbox: &RadrootsCliSandbox,
    797     account_id: &str,
    798     document_seller_pubkey: Option<&str>,
    799     owner_pubkey: Option<&str>,
    800     farm_d_tag: &str,
    801     listing_d_tag: &str,
    802     record_suffix: &str,
    803     title: &str,
    804     exportability: Option<serde_json::Value>,
    805     include_listing_addr: bool,
    806 ) -> String {
    807     let record_id = format!("app:local_work:listing:{listing_d_tag}:{record_suffix}");
    808     let seller_pubkey_json = document_seller_pubkey
    809         .map(|value| json!(value))
    810         .unwrap_or_else(|| json!(null));
    811     let mut payload = json!({
    812         "record_kind": "listing_draft_v1",
    813         "document": {
    814             "version": 1,
    815             "kind": "listing_draft_v1",
    816             "listing": {
    817                 "d_tag": listing_d_tag,
    818                 "farm_d_tag": farm_d_tag,
    819             },
    820             "seller_actor": {
    821                 "account_id": account_id,
    822                 "pubkey": seller_pubkey_json,
    823                 "source": "farm_config",
    824             },
    825             "product": {
    826                 "key": listing_d_tag,
    827                 "title": title,
    828                 "category": "eggs",
    829                 "summary": "Fresh app eggs",
    830             },
    831             "primary_bin": {
    832                 "bin_id": "bin-1",
    833                 "quantity_amount": "1",
    834                 "quantity_unit": "dozen",
    835                 "price_amount": "7.50",
    836                 "price_currency": "USD",
    837                 "price_per_amount": "1",
    838                 "price_per_unit": "dozen",
    839             },
    840             "inventory": {
    841                 "available": "12",
    842             },
    843             "availability": {
    844                 "kind": "local",
    845                 "status": "draft",
    846             },
    847             "delivery": {
    848                 "method": "pickup",
    849             },
    850             "location": {
    851                 "primary": "farmstand",
    852             },
    853         },
    854     });
    855     if let Some(exportability) = exportability {
    856         payload["exportability"] = exportability;
    857     }
    858     append_app_local_record(
    859         LocalEventRecordInput {
    860             record_id: record_id.clone(),
    861             family: LocalRecordFamily::LocalWork,
    862             status: LocalRecordStatus::LocalSaved,
    863             source_runtime: SourceRuntime::App,
    864             created_at_ms: 1_779_000_002_000,
    865             inserted_at_ms: 1_779_000_002_000,
    866             owner_account_id: Some(account_id.to_owned()),
    867             owner_pubkey: owner_pubkey.map(str::to_owned),
    868             farm_id: Some(farm_d_tag.to_owned()),
    869             listing_addr: include_listing_addr
    870                 .then_some(owner_pubkey)
    871                 .flatten()
    872                 .map(|owner_pubkey| format!("30402:{owner_pubkey}:{listing_d_tag}")),
    873             local_work_json: Some(payload),
    874             event_id: None,
    875             event_kind: None,
    876             event_pubkey: None,
    877             event_created_at: None,
    878             event_tags_json: None,
    879             event_content: None,
    880             event_sig: None,
    881             raw_event_json: None,
    882             outbox_status: PublishOutboxStatus::None,
    883             relay_set_fingerprint: None,
    884             relay_delivery_json: None,
    885         },
    886         sandbox,
    887     );
    888     record_id
    889 }
    890 
    891 fn append_app_local_record(input: LocalEventRecordInput, sandbox: &RadrootsCliSandbox) {
    892     let database_path = sandbox.local_events_db_path();
    893     fs::create_dir_all(database_path.parent().expect("local events parent"))
    894         .expect("local events parent");
    895     let executor = SqliteExecutor::open(database_path).expect("open local events");
    896     let store = LocalEventsStore::new(executor);
    897     store.migrate_up().expect("migrate local events");
    898     store
    899         .append_record(&input)
    900         .expect("append app local event record");
    901 }
    902 
    903 fn seed_app_order_record(
    904     sandbox: &RadrootsCliSandbox,
    905     account_id: &str,
    906     buyer_pubkey: &str,
    907     seller_pubkey: &str,
    908     order_id: &str,
    909     listing_addr: &str,
    910     listing_event_id: &str,
    911 ) -> String {
    912     seed_app_order_record_variant(
    913         sandbox,
    914         account_id,
    915         buyer_pubkey,
    916         seller_pubkey,
    917         order_id,
    918         listing_addr,
    919         listing_event_id,
    920         true,
    921         "supported",
    922         Vec::new(),
    923     )
    924 }
    925 
    926 fn seed_app_order_record_variant(
    927     sandbox: &RadrootsCliSandbox,
    928     account_id: &str,
    929     buyer_pubkey: &str,
    930     seller_pubkey: &str,
    931     order_id: &str,
    932     listing_addr: &str,
    933     listing_event_id: &str,
    934     current: bool,
    935     support_state: &str,
    936     support_issues: Vec<&str>,
    937 ) -> String {
    938     let record_id = format!("app:local_work:order_request:{order_id}");
    939     seed_app_order_record_variant_with_record_id(
    940         sandbox,
    941         account_id,
    942         buyer_pubkey,
    943         seller_pubkey,
    944         order_id,
    945         listing_addr,
    946         listing_event_id,
    947         record_id,
    948         current,
    949         support_state,
    950         support_issues,
    951     )
    952 }
    953 
    954 fn seed_app_order_record_variant_with_record_id(
    955     sandbox: &RadrootsCliSandbox,
    956     account_id: &str,
    957     buyer_pubkey: &str,
    958     seller_pubkey: &str,
    959     order_id: &str,
    960     listing_addr: &str,
    961     listing_event_id: &str,
    962     record_id: String,
    963     current: bool,
    964     support_state: &str,
    965     support_issues: Vec<&str>,
    966 ) -> String {
    967     let support_issues = support_issues
    968         .into_iter()
    969         .map(|issue| Value::String(issue.to_owned()))
    970         .collect::<Vec<_>>();
    971     let payload = json!({
    972         "record_kind": BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND,
    973         "scope": "app",
    974         "exportability": {
    975             "state": "exportable",
    976         },
    977         "support_status": {
    978             "state": support_state,
    979             "issues": support_issues,
    980         },
    981         "currentness": {
    982             "current": current,
    983             "source": "app_sqlite_order",
    984             "record_id": record_id,
    985             "order_id": order_id,
    986             "order_updated_at": "2026-05-24T12:00:10Z",
    987             "created_at_ms": 1_779_000_010_000_i64,
    988         },
    989         "document": {
    990             "version": 1,
    991             "kind": "order_draft_v1",
    992             "order": {
    993                 "order_id": order_id,
    994                 "listing_addr": listing_addr,
    995                 "listing_event_id": listing_event_id,
    996                 "listing_relays": [ORDERABLE_LISTING_RELAY],
    997                 "buyer_pubkey": buyer_pubkey,
    998                 "seller_pubkey": seller_pubkey,
    999                 "items": [
   1000                     {
   1001                         "bin_id": "bin-1",
   1002                         "bin_count": 2,
   1003                     }
   1004                 ],
   1005                 "economics": {
   1006                     "quote_id": format!("app-order:{order_id}"),
   1007                     "quote_version": 1,
   1008                     "pricing_basis": "listing_event",
   1009                     "currency": "USD",
   1010                     "items": [
   1011                         {
   1012                             "bin_id": "bin-1",
   1013                             "bin_count": 2,
   1014                             "quantity_amount": "1",
   1015                             "quantity_unit": "each",
   1016                             "unit_price_amount": "6",
   1017                             "unit_price_currency": "USD",
   1018                             "line_subtotal": {
   1019                                 "amount": "12",
   1020                                 "currency": "USD",
   1021                             },
   1022                         }
   1023                     ],
   1024                     "discounts": [],
   1025                     "adjustments": [],
   1026                     "subtotal": {
   1027                         "amount": "12",
   1028                         "currency": "USD",
   1029                     },
   1030                     "discount_total": {
   1031                         "amount": "0",
   1032                         "currency": "USD",
   1033                     },
   1034                     "adjustment_total": {
   1035                         "amount": "0",
   1036                         "currency": "USD",
   1037                     },
   1038                     "total": {
   1039                         "amount": "12",
   1040                         "currency": "USD",
   1041                     },
   1042                 },
   1043             },
   1044             "buyer_actor": {
   1045                 "account_id": account_id,
   1046                 "pubkey": buyer_pubkey,
   1047                 "source": "resolved_account",
   1048             },
   1049             "listing_lookup": listing_addr,
   1050         },
   1051         "app_order": {
   1052             "order_id": order_id,
   1053             "order_number": 1,
   1054             "farm_id": "018f47a8-7b2c-7000-8000-0000000000f1",
   1055             "farm_display_name": "CLI Interop Farm",
   1056             "farm_key": "pasture-eggs",
   1057             "status": "placed",
   1058             "buyer_context_key": "buyer_context",
   1059             "lines": [
   1060                 {
   1061                     "line_id": format!("{order_id}:product-eggs"),
   1062                     "product_id": "product-eggs",
   1063                     "listing_addr": listing_addr,
   1064                     "listing_event_id": listing_event_id,
   1065                     "seller_pubkey": seller_pubkey,
   1066                 }
   1067             ],
   1068         },
   1069     });
   1070     append_app_local_record(
   1071         LocalEventRecordInput {
   1072             record_id: record_id.clone(),
   1073             family: LocalRecordFamily::LocalWork,
   1074             status: LocalRecordStatus::LocalSaved,
   1075             source_runtime: SourceRuntime::App,
   1076             created_at_ms: 1_779_000_010_000,
   1077             inserted_at_ms: 1_779_000_010_000,
   1078             owner_account_id: Some(account_id.to_owned()),
   1079             owner_pubkey: Some(buyer_pubkey.to_owned()),
   1080             farm_id: Some("018f47a8-7b2c-7000-8000-0000000000f1".to_owned()),
   1081             listing_addr: Some(listing_addr.to_owned()),
   1082             local_work_json: Some(payload),
   1083             event_id: None,
   1084             event_kind: None,
   1085             event_pubkey: None,
   1086             event_created_at: None,
   1087             event_tags_json: None,
   1088             event_content: None,
   1089             event_sig: None,
   1090             raw_event_json: None,
   1091             outbox_status: PublishOutboxStatus::None,
   1092             relay_set_fingerprint: None,
   1093             relay_delivery_json: None,
   1094         },
   1095         sandbox,
   1096     );
   1097     record_id
   1098 }
   1099 
   1100 fn app_order_economics(order_id: &str, bin_count: u32) -> RadrootsOrderEconomics {
   1101     let line_total = (bin_count * 6).to_string();
   1102     serde_json::from_value(json!({
   1103         "quote_id": format!("app-order:{order_id}"),
   1104         "quote_version": 1,
   1105         "pricing_basis": "listing_event",
   1106         "currency": "USD",
   1107         "items": [
   1108             {
   1109                 "bin_id": "bin-1",
   1110                 "bin_count": bin_count,
   1111                 "quantity_amount": "1",
   1112                 "quantity_unit": "each",
   1113                 "unit_price_amount": "6",
   1114                 "unit_price_currency": "USD",
   1115                 "line_subtotal": {
   1116                     "amount": line_total,
   1117                     "currency": "USD",
   1118                 },
   1119             }
   1120         ],
   1121         "discounts": [],
   1122         "adjustments": [],
   1123         "subtotal": {
   1124             "amount": line_total,
   1125             "currency": "USD",
   1126         },
   1127         "discount_total": {
   1128             "amount": "0",
   1129             "currency": "USD",
   1130         },
   1131         "adjustment_total": {
   1132             "amount": "0",
   1133             "currency": "USD",
   1134         },
   1135         "total": {
   1136             "amount": line_total,
   1137             "currency": "USD",
   1138         },
   1139     }))
   1140     .expect("app order economics")
   1141 }
   1142 
   1143 fn signed_app_order_request_event(
   1144     buyer: &radroots_identity::RadrootsIdentity,
   1145     order_id: &str,
   1146     listing_addr: &str,
   1147     listing_event_id: &str,
   1148     seller_pubkey: &str,
   1149     bin_count: u32,
   1150 ) -> RadrootsNostrEvent {
   1151     let payload = RadrootsOrderRequest {
   1152         order_id: test_order_id(order_id),
   1153         listing_addr: test_listing_addr(listing_addr),
   1154         buyer_pubkey: test_pubkey(buyer.public_key_hex().as_str()),
   1155         seller_pubkey: test_pubkey(seller_pubkey),
   1156         items: vec![RadrootsOrderItem {
   1157             bin_id: test_inventory_bin_id("bin-1"),
   1158             bin_count,
   1159         }],
   1160         economics: app_order_economics(order_id, bin_count),
   1161     };
   1162     let parts = order_request_event_build(
   1163         &RadrootsNostrEventPtr {
   1164             id: listing_event_id.to_owned(),
   1165             relays: None,
   1166         },
   1167         &payload,
   1168     )
   1169     .expect("app order request parts");
   1170     radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
   1171         .expect("nostr event builder")
   1172         .sign_with_keys(buyer.keys())
   1173         .expect("signed app order request")
   1174 }
   1175 
   1176 fn append_app_signed_order_request_record(
   1177     sandbox: &RadrootsCliSandbox,
   1178     account_id: &str,
   1179     listing_addr: &str,
   1180     event: &RadrootsNostrEvent,
   1181 ) -> String {
   1182     let event_id = event.id.to_hex();
   1183     let event_tags = event
   1184         .tags
   1185         .iter()
   1186         .map(|tag| tag.as_slice().to_vec())
   1187         .collect::<Vec<_>>();
   1188     let delivery = RelayDeliveryEvidence::acknowledged(
   1189         [ORDERABLE_LISTING_RELAY],
   1190         [ORDERABLE_LISTING_RELAY],
   1191         [ORDERABLE_LISTING_RELAY],
   1192         Vec::new(),
   1193     )
   1194     .expect("order request delivery evidence");
   1195     let record_id = format!("app:signed_event:{event_id}");
   1196     append_app_local_record(
   1197         LocalEventRecordInput {
   1198             record_id: record_id.clone(),
   1199             family: LocalRecordFamily::SignedEvent,
   1200             status: LocalRecordStatus::Published,
   1201             source_runtime: SourceRuntime::App,
   1202             created_at_ms: i64::try_from(event.created_at.as_secs()).expect("event created_at")
   1203                 * 1_000,
   1204             inserted_at_ms: 1_779_000_011_000,
   1205             owner_account_id: Some(account_id.to_owned()),
   1206             owner_pubkey: Some(event.pubkey.to_string()),
   1207             farm_id: Some("018f47a8-7b2c-7000-8000-0000000000f1".to_owned()),
   1208             listing_addr: Some(listing_addr.to_owned()),
   1209             local_work_json: None,
   1210             event_id: Some(event_id),
   1211             event_kind: Some(i64::from(KIND_ORDER_REQUEST)),
   1212             event_pubkey: Some(event.pubkey.to_string()),
   1213             event_created_at: Some(
   1214                 i64::try_from(event.created_at.as_secs()).expect("event created_at"),
   1215             ),
   1216             event_tags_json: Some(json!(event_tags.clone())),
   1217             event_content: Some(event.content.clone()),
   1218             event_sig: Some(event.sig.to_string()),
   1219             raw_event_json: Some(json!({
   1220                 "id": event.id.to_hex(),
   1221                 "pubkey": event.pubkey.to_string(),
   1222                 "created_at": i64::try_from(event.created_at.as_secs()).expect("event created_at"),
   1223                 "kind": u32::from(event.kind.as_u16()),
   1224                 "tags": event_tags,
   1225                 "content": event.content.clone(),
   1226                 "sig": event.sig.to_string(),
   1227             })),
   1228             outbox_status: PublishOutboxStatus::Acknowledged,
   1229             relay_set_fingerprint: canonical_relay_set_fingerprint([ORDERABLE_LISTING_RELAY]),
   1230             relay_delivery_json: Some(delivery.to_json_value().expect("delivery json")),
   1231         },
   1232         sandbox,
   1233     );
   1234     record_id
   1235 }
   1236 
   1237 #[test]
   1238 fn root_help_exposes_only_target_namespaces() {
   1239     let output = radroots().arg("--help").output().expect("run root help");
   1240 
   1241     assert!(output.status.success());
   1242     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   1243     for namespace in [
   1244         "workspace",
   1245         "health",
   1246         "config",
   1247         "account",
   1248         "signer",
   1249         "relay",
   1250         "store",
   1251         "sync",
   1252         "farm",
   1253         "listing",
   1254         "market",
   1255         "basket",
   1256         "order",
   1257     ] {
   1258         assert!(
   1259             help_lists(&stdout, namespace),
   1260             "root help should contain `{namespace}`"
   1261         );
   1262     }
   1263 
   1264     for removed in [
   1265         "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "product",
   1266         "runtime", "job", "message", "approval", "agent",
   1267     ] {
   1268         assert!(
   1269             !help_lists(&stdout, removed),
   1270             "root help should not contain `{removed}`"
   1271         );
   1272     }
   1273 }
   1274 
   1275 #[test]
   1276 fn root_help_explains_publish_transports() {
   1277     let output = radroots().arg("--help").output().expect("run root help");
   1278 
   1279     assert!(output.status.success());
   1280     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   1281 
   1282     assert!(stdout.contains("direct_nostr_relay publishes directly to configured relays"));
   1283     assert!(stdout.contains("radrootsd_proxy publishes locally signed events"));
   1284     assert!(stdout.contains("Inspect local readiness and mode-specific recovery steps"));
   1285     assert!(stdout.contains(
   1286         "Select direct_nostr_relay direct relay publish or radrootsd_proxy daemon proxy publish"
   1287     ));
   1288 }
   1289 
   1290 fn help_lists(stdout: &str, command: &str) -> bool {
   1291     stdout.lines().any(|line| {
   1292         let line = line.trim_start();
   1293         line == command || line.starts_with(&format!("{command} "))
   1294     })
   1295 }
   1296 
   1297 #[test]
   1298 fn removed_global_flags_are_rejected_publicly() {
   1299     for args in [
   1300         ["--output", "json", "workspace", "get"].as_slice(),
   1301         ["--json", "workspace", "get"].as_slice(),
   1302         ["--ndjson", "workspace", "get"].as_slice(),
   1303         ["--yes", "workspace", "get"].as_slice(),
   1304         ["--non-interactive", "workspace", "get"].as_slice(),
   1305         ["--signer", "myc", "workspace", "get"].as_slice(),
   1306         ["--farm-id", "farm_test", "workspace", "get"].as_slice(),
   1307         ["--profile", "repo_local", "workspace", "get"].as_slice(),
   1308         ["--signer-session-id", "session_test", "workspace", "get"].as_slice(),
   1309     ] {
   1310         let output = radroots().args(args).output().expect("run removed flag");
   1311 
   1312         assert!(!output.status.success(), "`{args:?}` should be rejected");
   1313         let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
   1314         assert!(stderr.contains("unexpected argument") || stderr.contains("unrecognized"));
   1315     }
   1316 }
   1317 
   1318 #[test]
   1319 fn config_get_exposes_radrootsd_proxy_missing_token_state() {
   1320     let sandbox = RadrootsCliSandbox::new();
   1321     sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
   1322 
   1323     let value = sandbox.json_success(&["--format", "json", "config", "get"]);
   1324 
   1325     assert_eq!(value["operation_id"], "config.get");
   1326     assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
   1327     assert_eq!(
   1328         value["result"]["publish"]["source"],
   1329         "user config ยท local first"
   1330     );
   1331     assert_eq!(
   1332         value["result"]["publish"]["transport_family"],
   1333         "radrootsd_proxy"
   1334     );
   1335     assert_eq!(value["result"]["publish"]["state"], "unconfigured");
   1336     assert_eq!(value["result"]["publish"]["executable"], false);
   1337     assert_contains(
   1338         &value["result"]["publish"]["reason"],
   1339         "configured token file or token secret id",
   1340     );
   1341     assert_eq!(
   1342         value["result"]["account_resolution"]["status"],
   1343         "unresolved"
   1344     );
   1345     assert_eq!(
   1346         value["result"]["publish"]["provider"]["provider_runtime_id"],
   1347         "radrootsd_proxy"
   1348     );
   1349     assert_eq!(
   1350         value["result"]["write_plane"]["provider_runtime_id"],
   1351         "radrootsd_proxy"
   1352     );
   1353     assert_eq!(
   1354         value["result"]["write_plane"]["binding_model"],
   1355         "daemon_proxy_publish"
   1356     );
   1357     assert_eq!(value["result"]["write_plane"]["state"], "unconfigured");
   1358     assert_eq!(
   1359         value["result"]["radrootsd_proxy"]["token_file_configured"],
   1360         false
   1361     );
   1362     assert_eq!(
   1363         value["result"]["radrootsd_proxy"]["token_secret_id_configured"],
   1364         false
   1365     );
   1366     assert_eq!(
   1367         value["result"]["actions"][0],
   1368         "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
   1369     );
   1370     assert_eq!(
   1371         value["next_actions"][0]["env_var"],
   1372         "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE"
   1373     );
   1374 }
   1375 
   1376 #[test]
   1377 fn config_get_radrootsd_proxy_with_token_file_reports_ready_transport() {
   1378     let sandbox = RadrootsCliSandbox::new();
   1379     sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
   1380     let token_file = radrootsd_proxy_token_file(&sandbox);
   1381 
   1382     let mut command = sandbox.command();
   1383     command
   1384         .env("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE", token_file)
   1385         .args(["--format", "json", "config", "get"]);
   1386     let output = command.output().expect("run config get");
   1387     let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   1388 
   1389     assert!(output.status.success());
   1390     assert_eq!(value["operation_id"], "config.get");
   1391     assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
   1392     assert_eq!(value["result"]["publish"]["state"], "ready");
   1393     assert_eq!(value["result"]["publish"]["executable"], true);
   1394     assert_eq!(value["result"]["publish"]["reason"], Value::Null);
   1395     assert_eq!(
   1396         value["result"]["radrootsd_proxy"]["token_file_configured"],
   1397         true
   1398     );
   1399     assert_eq!(
   1400         value["result"]["actions"]
   1401             .as_array()
   1402             .expect("actions")
   1403             .len(),
   1404         0
   1405     );
   1406 }
   1407 
   1408 #[test]
   1409 fn config_get_marks_radrootsd_proxy_unconfigured_with_incomplete_myc_signer() {
   1410     let sandbox = RadrootsCliSandbox::new();
   1411     sandbox.write_app_config(
   1412         r#"[publish]
   1413 transport = "radrootsd_proxy"
   1414 
   1415 [signer]
   1416 backend = "myc"
   1417 
   1418 [[capability_binding]]
   1419 capability = "signer.remote_nip46"
   1420 provider = "myc"
   1421 target_kind = "explicit_endpoint"
   1422 target = "http://myc.invalid"
   1423 signer_session_ref = "session_ready"
   1424 "#,
   1425     );
   1426     let token_file = radrootsd_proxy_token_file(&sandbox);
   1427 
   1428     let mut command = sandbox.command();
   1429     command
   1430         .env("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE", token_file)
   1431         .args(["--format", "json", "config", "get"]);
   1432     let output = command.output().expect("run config get");
   1433     let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   1434 
   1435     assert!(output.status.success());
   1436     assert_eq!(value["operation_id"], "config.get");
   1437     assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
   1438     assert_eq!(value["result"]["publish"]["state"], "unconfigured");
   1439     assert_eq!(value["result"]["publish"]["executable"], false);
   1440     assert_contains(&value["result"]["publish"]["reason"], "signer.remote_nip46");
   1441     assert_eq!(
   1442         value["result"]["publish"]["provider"]["state"],
   1443         "unconfigured"
   1444     );
   1445     assert_eq!(value["result"]["actions"][0], "radroots signer status get");
   1446 }
   1447 
   1448 #[test]
   1449 fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
   1450     let sandbox = RadrootsCliSandbox::new();
   1451 
   1452     let value = sandbox.json_success(&[
   1453         "--format",
   1454         "json",
   1455         "--relay",
   1456         "ws://127.0.0.1:19001",
   1457         "config",
   1458         "get",
   1459     ]);
   1460 
   1461     assert_eq!(value["operation_id"], "config.get");
   1462     assert_eq!(
   1463         value["result"]["publish"]["transport"],
   1464         "direct_nostr_relay"
   1465     );
   1466     assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
   1467     assert_eq!(value["result"]["publish"]["signed_write_required"], true);
   1468     assert_eq!(value["result"]["publish"]["state"], "unconfigured");
   1469     assert_eq!(value["result"]["publish"]["executable"], false);
   1470     assert_contains(
   1471         &value["result"]["publish"]["reason"],
   1472         "write-capable local account",
   1473     );
   1474     assert_eq!(
   1475         value["result"]["publish"]["provider"]["state"],
   1476         "unconfigured"
   1477     );
   1478     assert_eq!(
   1479         value["result"]["write_plane"]["provider_runtime_id"],
   1480         "direct_nostr_relay"
   1481     );
   1482     assert_eq!(
   1483         value["result"]["write_plane"]["binding_model"],
   1484         "direct_relay_publish"
   1485     );
   1486     assert_eq!(value["result"]["write_plane"]["state"], "unconfigured");
   1487     assert_eq!(value["result"]["rpc"], Value::Null);
   1488     assert_contains(
   1489         &value["result"]["write_plane"]["detail"],
   1490         "write-capable local account",
   1491     );
   1492     assert_eq!(value["result"]["actions"][0], "radroots account create");
   1493     assert_eq!(
   1494         value["next_actions"][0]["command"],
   1495         "radroots account create"
   1496     );
   1497     assert_no_daemon_runtime_reference(
   1498         &value,
   1499         &[
   1500             "--format",
   1501             "json",
   1502             "--relay",
   1503             "ws://127.0.0.1:19001",
   1504             "config",
   1505             "get",
   1506         ],
   1507     );
   1508 }
   1509 
   1510 #[test]
   1511 fn config_get_marks_relay_publish_ready_with_secret_backed_local_account() {
   1512     let sandbox = RadrootsCliSandbox::new();
   1513     sandbox.json_success(&["--format", "json", "account", "create"]);
   1514 
   1515     let value = sandbox.json_success(&[
   1516         "--format",
   1517         "json",
   1518         "--relay",
   1519         "ws://127.0.0.1:19002",
   1520         "config",
   1521         "get",
   1522     ]);
   1523 
   1524     assert_eq!(
   1525         value["result"]["publish"]["transport"],
   1526         "direct_nostr_relay"
   1527     );
   1528     assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
   1529     assert_eq!(value["result"]["publish"]["signed_write_required"], true);
   1530     assert_eq!(value["result"]["publish"]["state"], "ready");
   1531     assert_eq!(value["result"]["publish"]["executable"], true);
   1532     assert_eq!(value["result"]["publish"]["reason"], Value::Null);
   1533     assert_eq!(value["result"]["publish"]["provider"]["state"], "ready");
   1534 }
   1535 
   1536 #[test]
   1537 fn config_get_marks_relay_publish_unconfigured_with_missing_myc_binding() {
   1538     let sandbox = RadrootsCliSandbox::new();
   1539     sandbox.json_success(&["--format", "json", "account", "create"]);
   1540     sandbox.write_app_config("[signer]\nbackend = \"myc\"\n");
   1541 
   1542     let value = sandbox.json_success(&[
   1543         "--format",
   1544         "json",
   1545         "--relay",
   1546         "ws://127.0.0.1:19003",
   1547         "config",
   1548         "get",
   1549     ]);
   1550 
   1551     assert_eq!(
   1552         value["result"]["publish"]["transport"],
   1553         "direct_nostr_relay"
   1554     );
   1555     assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
   1556     assert_eq!(value["result"]["publish"]["signed_write_required"], true);
   1557     assert_eq!(value["result"]["publish"]["state"], "unconfigured");
   1558     assert_eq!(value["result"]["publish"]["executable"], false);
   1559     assert_contains(
   1560         &value["result"]["publish"]["reason"],
   1561         "signer.remote_nip46 binding is missing",
   1562     );
   1563     assert_eq!(
   1564         value["result"]["publish"]["provider"]["state"],
   1565         "unconfigured"
   1566     );
   1567 }
   1568 
   1569 #[test]
   1570 fn config_get_marks_relay_publish_unconfigured_with_watch_only_account() {
   1571     let sandbox = RadrootsCliSandbox::new();
   1572     let public_identity = identity_public(41);
   1573     let public_identity_file =
   1574         write_public_identity_profile(&sandbox, "publish-readiness-watch-only", &public_identity);
   1575     sandbox.json_success(&[
   1576         "--format",
   1577         "json",
   1578         "--approval-token",
   1579         "approve",
   1580         "account",
   1581         "import",
   1582         "--default",
   1583         public_identity_file.to_string_lossy().as_ref(),
   1584     ]);
   1585 
   1586     let value = sandbox.json_success(&[
   1587         "--format",
   1588         "json",
   1589         "--relay",
   1590         "ws://127.0.0.1:19004",
   1591         "config",
   1592         "get",
   1593     ]);
   1594 
   1595     assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
   1596     assert_eq!(value["result"]["publish"]["signed_write_required"], true);
   1597     assert_eq!(value["result"]["publish"]["state"], "unconfigured");
   1598     assert_eq!(value["result"]["publish"]["executable"], false);
   1599     assert_contains(&value["result"]["publish"]["reason"], "watch_only");
   1600 }
   1601 
   1602 #[test]
   1603 fn health_surfaces_publish_state_under_missing_myc_binding() {
   1604     let sandbox = RadrootsCliSandbox::new();
   1605     let missing_myc = sandbox.root().join("bin/missing-myc");
   1606     let token_file = radrootsd_proxy_token_file(&sandbox);
   1607     sandbox.write_app_config(&format!(
   1608         "[publish]\ntransport = \"radrootsd_proxy\"\n\n[publish.radrootsd_proxy]\ntoken_file = \"{}\"\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
   1609         toml_string(token_file.display().to_string().as_str()),
   1610         toml_string(missing_myc.display().to_string().as_str())
   1611     ));
   1612 
   1613     let value = sandbox.json_success(&["--format", "json", "health", "status", "get"]);
   1614 
   1615     assert_eq!(value["operation_id"], "health.status.get");
   1616     assert_eq!(value["result"]["state"], "needs_attention");
   1617     assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
   1618     assert_eq!(value["result"]["publish"]["executable"], false);
   1619     assert_eq!(
   1620         value["result"]["publish"]["provider"]["state"],
   1621         "unconfigured"
   1622     );
   1623     assert_contains(
   1624         &value["result"]["publish"]["reason"],
   1625         "signer.remote_nip46 binding is missing",
   1626     );
   1627     assert_eq!(value["result"]["store"]["state"], "ready");
   1628     assert_eq!(
   1629         value["result"]["store"]["source"],
   1630         "SDK canonical event store and outbox"
   1631     );
   1632     assert_eq!(value["result"]["store"]["canonical_store"], "sdk");
   1633     assert_eq!(value["result"]["signer"]["state"], "unconfigured");
   1634     assert_eq!(value["result"]["actions"][0], "radroots account create");
   1635     assert_eq!(value["result"]["actions"][1], "radroots signer status get");
   1636     assert_eq!(
   1637         value["next_actions"][0]["command"],
   1638         "radroots account create"
   1639     );
   1640     assert_eq!(
   1641         value["next_actions"][1]["command"],
   1642         "radroots signer status get"
   1643     );
   1644     assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
   1645 }
   1646 
   1647 #[test]
   1648 fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() {
   1649     let sandbox = RadrootsCliSandbox::new();
   1650 
   1651     let value = sandbox.json_success(&[
   1652         "--format",
   1653         "json",
   1654         "--relay",
   1655         "ws://127.0.0.1:19005",
   1656         "health",
   1657         "status",
   1658         "get",
   1659     ]);
   1660 
   1661     assert_eq!(value["operation_id"], "health.status.get");
   1662     assert_eq!(value["result"]["state"], "needs_attention");
   1663     assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
   1664     assert_eq!(value["result"]["publish"]["signed_write_required"], true);
   1665     assert_eq!(value["result"]["publish"]["state"], "unconfigured");
   1666     assert_eq!(value["result"]["publish"]["executable"], false);
   1667     assert_contains(
   1668         &value["result"]["publish"]["reason"],
   1669         "write-capable local account",
   1670     );
   1671     assert_eq!(value["result"]["store"]["state"], "ready");
   1672     assert_eq!(value["result"]["store"]["canonical_store"], "sdk");
   1673     assert_eq!(value["result"]["signer"]["state"], "unconfigured");
   1674     assert_eq!(value["result"]["actions"][0], "radroots account create");
   1675     assert_eq!(
   1676         value["next_actions"][0]["command"],
   1677         "radroots account create"
   1678     );
   1679 }
   1680 
   1681 #[test]
   1682 fn health_check_exposes_publish_readiness() {
   1683     let sandbox = RadrootsCliSandbox::new();
   1684     sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
   1685 
   1686     let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]);
   1687 
   1688     assert_eq!(value["operation_id"], "health.check.run");
   1689     assert_eq!(value["result"]["state"], "needs_attention");
   1690     assert_eq!(
   1691         value["result"]["account_resolution"]["status"],
   1692         "unresolved"
   1693     );
   1694     assert_eq!(value["result"]["account_resolution"]["source"], "none");
   1695     assert_eq!(
   1696         value["result"]["checks"]["publish"]["transport"],
   1697         "radrootsd_proxy"
   1698     );
   1699     assert_eq!(
   1700         value["result"]["checks"]["publish"]["state"],
   1701         "unconfigured"
   1702     );
   1703     assert_eq!(value["result"]["checks"]["publish"]["executable"], false);
   1704     assert_contains(
   1705         &value["result"]["checks"]["publish"]["reason"],
   1706         "configured token file or token secret id",
   1707     );
   1708     assert_eq!(value["result"]["checks"]["store"]["state"], "ready");
   1709     assert_eq!(
   1710         value["result"]["checks"]["store"]["source"],
   1711         "SDK canonical event store and outbox"
   1712     );
   1713     assert_eq!(value["result"]["checks"]["store"]["canonical_store"], "sdk");
   1714     assert_eq!(value["result"]["checks"]["signer"]["state"], "unconfigured");
   1715     assert_eq!(value["result"]["actions"][0], "radroots account create");
   1716     assert_eq!(
   1717         value["result"]["actions"][1],
   1718         "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
   1719     );
   1720     assert_eq!(
   1721         value["next_actions"][0]["command"],
   1722         "radroots account create"
   1723     );
   1724     assert_eq!(
   1725         value["next_actions"][1]["description"],
   1726         "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
   1727     );
   1728     assert_eq!(
   1729         value["next_actions"][1]["env_var"],
   1730         "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE"
   1731     );
   1732     assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
   1733 }
   1734 
   1735 #[test]
   1736 fn health_check_marks_relay_publish_ready_with_secret_backed_local_account() {
   1737     let sandbox = RadrootsCliSandbox::new();
   1738     sandbox.json_success(&["--format", "json", "workspace", "init"]);
   1739     sandbox.json_success(&["--format", "json", "account", "create"]);
   1740 
   1741     let value = sandbox.json_success(&[
   1742         "--format",
   1743         "json",
   1744         "--relay",
   1745         "ws://127.0.0.1:19006",
   1746         "health",
   1747         "check",
   1748         "run",
   1749     ]);
   1750 
   1751     assert_eq!(value["operation_id"], "health.check.run");
   1752     assert_eq!(value["result"]["state"], "ready");
   1753     assert_eq!(value["result"]["account_resolution"]["status"], "resolved");
   1754     assert_eq!(
   1755         value["result"]["account_resolution"]["source"],
   1756         "default_account"
   1757     );
   1758     assert_eq!(
   1759         value["result"]["account_resolution"]["resolved_account"]["custody"],
   1760         "secret_backed"
   1761     );
   1762     assert_eq!(
   1763         value["result"]["account_resolution"]["resolved_account"]["write_capable"],
   1764         true
   1765     );
   1766     assert_eq!(
   1767         value["result"]["checks"]["publish"]["transport"],
   1768         "direct_nostr_relay"
   1769     );
   1770     assert_eq!(value["result"]["checks"]["publish"]["state"], "ready");
   1771     assert_eq!(value["result"]["checks"]["publish"]["executable"], true);
   1772     assert_eq!(
   1773         value["result"]["actions"]
   1774             .as_array()
   1775             .expect("actions")
   1776             .len(),
   1777         0
   1778     );
   1779     assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
   1780 }
   1781 
   1782 #[test]
   1783 fn farm_readiness_check_reports_mode_specific_publish_gates() {
   1784     let sandbox = RadrootsCliSandbox::new();
   1785     sandbox.json_success(&["--format", "json", "account", "create"]);
   1786     sandbox.json_success(&[
   1787         "--format",
   1788         "json",
   1789         "farm",
   1790         "create",
   1791         "--name",
   1792         "Ready Farm",
   1793         "--location",
   1794         "farmstand",
   1795         "--country",
   1796         "US",
   1797         "--delivery-method",
   1798         "pickup",
   1799     ]);
   1800 
   1801     let (_, relay_value) = sandbox.json_output(&["--format", "json", "farm", "readiness", "check"]);
   1802     let relay_detail = if relay_value["result"].is_null() {
   1803         &relay_value["errors"][0]["detail"]
   1804     } else {
   1805         &relay_value["result"]
   1806     };
   1807     assert_eq!(relay_detail["publish_transport"], "direct_nostr_relay");
   1808     assert_eq!(relay_detail["publish_state"], "unconfigured");
   1809     assert_eq!(relay_detail["publish_executable"], false);
   1810     assert_eq!(relay_detail["missing"][0], "Configured relay");
   1811 
   1812     sandbox.write_app_config(
   1813         r#"[[capability_binding]]
   1814 capability = "signer.remote_nip46"
   1815 provider = "myc"
   1816 target_kind = "explicit_endpoint"
   1817 target = "http://myc.invalid"
   1818 signer_session_ref = "session_test"
   1819 "#,
   1820     );
   1821     let output = sandbox
   1822         .command()
   1823         .env(
   1824             "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE",
   1825             radrootsd_proxy_token_file(&sandbox),
   1826         )
   1827         .args([
   1828             "--format",
   1829             "json",
   1830             "--publish-transport",
   1831             "radrootsd_proxy",
   1832             "farm",
   1833             "readiness",
   1834             "check",
   1835         ])
   1836         .output()
   1837         .expect("run radrootsd proxy farm readiness");
   1838     let radrootsd_value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   1839 
   1840     assert!(output.status.success());
   1841     assert_eq!(radrootsd_value["operation_id"], "farm.readiness.check");
   1842     assert_contains(
   1843         &radrootsd_value["result"]["publish_transport"],
   1844         "radrootsd_proxy",
   1845     );
   1846     assert_eq!(radrootsd_value["result"]["publish_state"], "ready");
   1847     assert_eq!(radrootsd_value["result"]["publish_executable"], true);
   1848     assert_eq!(radrootsd_value["result"]["reason"], Value::Null);
   1849     assert_eq!(
   1850         radrootsd_value["result"]["actions"][0],
   1851         "radroots farm publish"
   1852     );
   1853 }
   1854 
   1855 #[test]
   1856 fn radrootsd_proxy_listing_publish_update_and_archive_dry_run_without_direct_relays() {
   1857     for operation in ["publish", "update", "archive"] {
   1858         let sandbox = RadrootsCliSandbox::new();
   1859         sandbox.json_success(&["--format", "json", "account", "create"]);
   1860         let farm = sandbox.json_success(&[
   1861             "--format",
   1862             "json",
   1863             "farm",
   1864             "create",
   1865             "--name",
   1866             "Proxy Farm",
   1867             "--location",
   1868             "farmstand",
   1869             "--country",
   1870             "US",
   1871             "--delivery-method",
   1872             "pickup",
   1873         ]);
   1874         let listing_file =
   1875             create_listing_draft(&sandbox, format!("radrootsd-proxy-{operation}").as_str());
   1876         make_listing_publishable(
   1877             &listing_file,
   1878             farm["result"]["config"]["farm_d_tag"]
   1879                 .as_str()
   1880                 .expect("farm d tag"),
   1881         );
   1882 
   1883         let output = sandbox
   1884             .command()
   1885             .env(
   1886                 "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE",
   1887                 radrootsd_proxy_token_file(&sandbox),
   1888             )
   1889             .args([
   1890                 "--format",
   1891                 "json",
   1892                 "--publish-transport",
   1893                 "radrootsd_proxy",
   1894                 "--dry-run",
   1895                 "listing",
   1896                 operation,
   1897                 listing_file.to_string_lossy().as_ref(),
   1898             ])
   1899             .output()
   1900             .expect("run proxy listing dry run");
   1901         let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   1902 
   1903         assert!(output.status.success());
   1904         assert_eq!(value["operation_id"], format!("listing.{operation}"));
   1905         assert_eq!(value["result"]["state"], "dry_run");
   1906         assert_eq!(
   1907             value["result"]["source"],
   1908             "SDK listing publish ยท configured signer"
   1909         );
   1910         assert_eq!(value["result"]["dry_run"], true);
   1911         assert_eq!(
   1912             value["result"]["target_relays"]
   1913                 .as_array()
   1914                 .expect("relays")
   1915                 .len(),
   1916             0
   1917         );
   1918         assert_contains(
   1919             &value["result"]["reason"],
   1920             "SDK enqueue and relay push skipped",
   1921         );
   1922     }
   1923 }
   1924 
   1925 #[test]
   1926 fn radrootsd_proxy_listing_publish_non_dry_run_uses_local_jsonrpc_server() {
   1927     let sandbox = RadrootsCliSandbox::new();
   1928     let proxy = RadrootsdProxyJsonRpcServer::once("proxy_test_token");
   1929     let token_file = radrootsd_proxy_token_file(&sandbox);
   1930     sandbox.write_app_config(
   1931         format!(
   1932             r#"[publish]
   1933 transport = "radrootsd_proxy"
   1934 
   1935 [publish.radrootsd_proxy]
   1936 url = "{}"
   1937 token_file = "{}"
   1938 "#,
   1939             toml_string(proxy.endpoint()),
   1940             toml_string(token_file.display().to_string().as_str())
   1941         )
   1942         .as_str(),
   1943     );
   1944     sandbox.json_success(&["--format", "json", "account", "create"]);
   1945     let farm = sandbox.json_success(&[
   1946         "--format",
   1947         "json",
   1948         "farm",
   1949         "create",
   1950         "--name",
   1951         "Proxy Farm",
   1952         "--location",
   1953         "farmstand",
   1954         "--country",
   1955         "US",
   1956         "--delivery-method",
   1957         "pickup",
   1958     ]);
   1959     let listing_file = create_listing_draft(&sandbox, "radrootsd-proxy-live");
   1960     make_listing_publishable(
   1961         &listing_file,
   1962         farm["result"]["config"]["farm_d_tag"]
   1963             .as_str()
   1964             .expect("farm d tag"),
   1965     );
   1966 
   1967     let output = sandbox
   1968         .command()
   1969         .args([
   1970             "--format",
   1971             "json",
   1972             "--approval-token",
   1973             "approve",
   1974             "listing",
   1975             "publish",
   1976             listing_file.to_string_lossy().as_ref(),
   1977         ])
   1978         .output()
   1979         .expect("run proxy listing publish");
   1980     let request = proxy.join();
   1981     let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   1982 
   1983     assert!(
   1984         output.status.success(),
   1985         "stderr `{}` stdout `{}`",
   1986         String::from_utf8_lossy(&output.stderr),
   1987         String::from_utf8_lossy(&output.stdout)
   1988     );
   1989     assert_eq!(value["operation_id"], "listing.publish");
   1990     assert_eq!(value["result"]["state"], "published");
   1991     assert_eq!(
   1992         value["result"]["source"],
   1993         "SDK listing publish ยท configured signer"
   1994     );
   1995     assert_eq!(value["result"]["dry_run"], false);
   1996     assert_eq!(
   1997         request["params"]["event"]["id"],
   1998         value["result"]["event_id"]
   1999     );
   2000     assert_eq!(
   2001         request["params"]["relays"]
   2002             .as_array()
   2003             .expect("relays")
   2004             .len(),
   2005         0
   2006     );
   2007     assert!(
   2008         request["params"]["idempotency_key"]
   2009             .as_str()
   2010             .expect("daemon idempotency key")
   2011             .contains("-1-")
   2012     );
   2013 
   2014     let status = sandbox.json_success(&["--format", "json", "sync", "status", "get"]);
   2015     assert_eq!(
   2016         status["result"]["source"],
   2017         "SDK canonical event store and outbox"
   2018     );
   2019     assert_eq!(status["result"]["queue"]["pending_count"], 0);
   2020     assert_eq!(status["result"]["queue"]["ready_signed_count"], 0);
   2021     assert_eq!(status["result"]["queue"]["publishing_count"], 0);
   2022     assert_eq!(status["result"]["queue"]["retryable_count"], 0);
   2023     assert_eq!(status["result"]["queue"]["failed_terminal_count"], 0);
   2024 }
   2025 
   2026 #[test]
   2027 fn direct_relay_listing_publish_uses_myc_nip46_sdk_signer() {
   2028     let sandbox = RadrootsCliSandbox::new();
   2029     let user_identity = identity_secret(90);
   2030     let client_identity = identity_secret(91);
   2031     let remote_identity = identity_secret(92);
   2032     let user_public = user_identity.to_public();
   2033     let user_keys = nostr_keys_from_identity(&user_identity);
   2034     let remote_keys = nostr_keys_from_identity(&remote_identity);
   2035     let remote_pubkey = remote_keys.public_key().to_hex();
   2036     let relay = Nip46RelayServer::new(
   2037         remote_keys.clone(),
   2038         user_keys,
   2039         Nip46RelayFinish::ProductPublish,
   2040     );
   2041     let relay_endpoint = relay.endpoint().to_owned();
   2042     let public_identity_file =
   2043         write_public_identity_profile(&sandbox, "myc-direct-user", &user_public);
   2044     let imported = sandbox.json_success(&[
   2045         "--format",
   2046         "json",
   2047         "--approval-token",
   2048         "approve",
   2049         "account",
   2050         "import",
   2051         "--default",
   2052         public_identity_file.to_string_lossy().as_ref(),
   2053     ]);
   2054     let account_id = imported["result"]["account"]["id"]
   2055         .as_str()
   2056         .expect("account id");
   2057     store_test_session_secret(
   2058         &sandbox,
   2059         "session_ready",
   2060         client_identity.secret_key_hex().as_str(),
   2061     );
   2062     sandbox.write_app_config(
   2063         myc_nip46_config(
   2064             remote_pubkey.as_str(),
   2065             relay_endpoint.as_str(),
   2066             account_id,
   2067             "session_ready",
   2068         )
   2069         .as_str(),
   2070     );
   2071     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   2072     assert_eq!(signer["result"]["state"], "ready");
   2073     assert_eq!(signer["result"]["binding"]["state"], "ready");
   2074     let farm = sandbox.json_success(&[
   2075         "--format",
   2076         "json",
   2077         "farm",
   2078         "create",
   2079         "--name",
   2080         "Myc Relay Farm",
   2081         "--location",
   2082         "farmstand",
   2083         "--country",
   2084         "US",
   2085         "--delivery-method",
   2086         "pickup",
   2087     ]);
   2088     let listing_file = create_listing_draft(&sandbox, "myc-direct-relay-live");
   2089     make_listing_publishable(
   2090         &listing_file,
   2091         farm["result"]["config"]["farm_d_tag"]
   2092             .as_str()
   2093             .expect("farm d tag"),
   2094     );
   2095 
   2096     let output = sandbox
   2097         .command()
   2098         .args([
   2099             "--format",
   2100             "json",
   2101             "--approval-token",
   2102             "approve",
   2103             "--relay",
   2104             relay_endpoint.as_str(),
   2105             "listing",
   2106             "publish",
   2107             listing_file.to_string_lossy().as_ref(),
   2108         ])
   2109         .output()
   2110         .expect("run myc direct listing publish");
   2111     let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   2112 
   2113     assert!(
   2114         output.status.success(),
   2115         "stderr `{}` stdout `{}`",
   2116         String::from_utf8_lossy(&output.stderr),
   2117         String::from_utf8_lossy(&output.stdout)
   2118     );
   2119     let report = relay.join();
   2120     assert_eq!(value["operation_id"], "listing.publish");
   2121     assert_eq!(value["result"]["state"], "published");
   2122     assert_eq!(
   2123         value["result"]["source"],
   2124         "SDK listing publish ยท configured signer"
   2125     );
   2126     assert_eq!(value["result"]["target_relays"][0], relay_endpoint);
   2127     assert!(report.connection_count >= 1);
   2128     assert!(report.req_count >= 1);
   2129     assert_eq!(report.sign_request_count, 1);
   2130     assert_eq!(report.published_events.len(), 1);
   2131     let published = &report.published_events[0];
   2132     assert_eq!(published.kind, Kind::Custom(KIND_LISTING as u16));
   2133     assert_eq!(published.pubkey.to_hex(), user_public.public_key_hex);
   2134     assert_eq!(
   2135         published.id.to_hex(),
   2136         value["result"]["event_id"].as_str().expect("event id")
   2137     );
   2138 }
   2139 
   2140 #[test]
   2141 fn radrootsd_proxy_listing_publish_uses_myc_nip46_sdk_signer() {
   2142     let sandbox = RadrootsCliSandbox::new();
   2143     let user_identity = identity_secret(93);
   2144     let client_identity = identity_secret(94);
   2145     let remote_identity = identity_secret(95);
   2146     let user_public = user_identity.to_public();
   2147     let user_keys = nostr_keys_from_identity(&user_identity);
   2148     let remote_keys = nostr_keys_from_identity(&remote_identity);
   2149     let remote_pubkey = remote_keys.public_key().to_hex();
   2150     let relay = Nip46RelayServer::new(
   2151         remote_keys.clone(),
   2152         user_keys,
   2153         Nip46RelayFinish::SignResponse,
   2154     );
   2155     let proxy = RadrootsdProxyJsonRpcServer::once("proxy_test_token");
   2156     let token_file = radrootsd_proxy_token_file(&sandbox);
   2157     let public_identity_file =
   2158         write_public_identity_profile(&sandbox, "myc-proxy-user", &user_public);
   2159     let imported = sandbox.json_success(&[
   2160         "--format",
   2161         "json",
   2162         "--approval-token",
   2163         "approve",
   2164         "account",
   2165         "import",
   2166         "--default",
   2167         public_identity_file.to_string_lossy().as_ref(),
   2168     ]);
   2169     let account_id = imported["result"]["account"]["id"]
   2170         .as_str()
   2171         .expect("account id");
   2172     store_test_session_secret(
   2173         &sandbox,
   2174         "session_ready",
   2175         client_identity.secret_key_hex().as_str(),
   2176     );
   2177     let config = format!(
   2178         r#"[publish]
   2179 transport = "radrootsd_proxy"
   2180 
   2181 [publish.radrootsd_proxy]
   2182 url = "{}"
   2183 token_file = "{}"
   2184 
   2185 {}"#,
   2186         toml_string(proxy.endpoint()),
   2187         toml_string(token_file.display().to_string().as_str()),
   2188         myc_nip46_config(
   2189             remote_pubkey.as_str(),
   2190             relay.endpoint(),
   2191             account_id,
   2192             "session_ready",
   2193         ),
   2194     );
   2195     sandbox.write_app_config(config.as_str());
   2196     let farm = sandbox.json_success(&[
   2197         "--format",
   2198         "json",
   2199         "farm",
   2200         "create",
   2201         "--name",
   2202         "Myc Proxy Farm",
   2203         "--location",
   2204         "farmstand",
   2205         "--country",
   2206         "US",
   2207         "--delivery-method",
   2208         "pickup",
   2209     ]);
   2210     let listing_file = create_listing_draft(&sandbox, "myc-radrootsd-proxy-live");
   2211     make_listing_publishable(
   2212         &listing_file,
   2213         farm["result"]["config"]["farm_d_tag"]
   2214             .as_str()
   2215             .expect("farm d tag"),
   2216     );
   2217 
   2218     let output = sandbox
   2219         .command()
   2220         .args([
   2221             "--format",
   2222             "json",
   2223             "--approval-token",
   2224             "approve",
   2225             "listing",
   2226             "publish",
   2227             listing_file.to_string_lossy().as_ref(),
   2228         ])
   2229         .output()
   2230         .expect("run myc proxy listing publish");
   2231     let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
   2232 
   2233     assert!(
   2234         output.status.success(),
   2235         "stderr `{}` stdout `{}`",
   2236         String::from_utf8_lossy(&output.stderr),
   2237         String::from_utf8_lossy(&output.stdout)
   2238     );
   2239     let report = relay.join();
   2240     let request = proxy.join();
   2241     assert_eq!(value["operation_id"], "listing.publish");
   2242     assert_eq!(value["result"]["state"], "published");
   2243     assert_eq!(
   2244         value["result"]["source"],
   2245         "SDK listing publish ยท configured signer"
   2246     );
   2247     assert!(report.connection_count >= 1);
   2248     assert!(report.req_count >= 1);
   2249     assert_eq!(report.sign_request_count, 1);
   2250     assert_eq!(report.published_events.len(), 0);
   2251     assert_eq!(request["params"]["event"]["kind"], KIND_LISTING);
   2252     assert_eq!(
   2253         request["params"]["event"]["pubkey"],
   2254         user_public.public_key_hex
   2255     );
   2256     assert_eq!(
   2257         request["params"]["event"]["id"],
   2258         value["result"]["event_id"]
   2259     );
   2260 }
   2261 
   2262 #[test]
   2263 fn listing_update_publish_attempts_direct_relay_with_approval() {
   2264     let sandbox = RadrootsCliSandbox::new();
   2265     sandbox.json_success(&["--format", "json", "account", "create"]);
   2266     let farm = sandbox.json_success(&[
   2267         "--format",
   2268         "json",
   2269         "farm",
   2270         "create",
   2271         "--name",
   2272         "Update Farm",
   2273         "--location",
   2274         "farmstand",
   2275         "--country",
   2276         "US",
   2277         "--delivery-method",
   2278         "pickup",
   2279     ]);
   2280     let listing_file = create_listing_draft(&sandbox, "update-unavailable");
   2281     make_listing_publishable(
   2282         &listing_file,
   2283         farm["result"]["config"]["farm_d_tag"]
   2284             .as_str()
   2285             .expect("farm d tag"),
   2286     );
   2287 
   2288     let (output, value) = sandbox.json_output(&[
   2289         "--format",
   2290         "json",
   2291         "--relay",
   2292         "ws://127.0.0.1:9",
   2293         "--approval-token",
   2294         "approve",
   2295         "listing",
   2296         "update",
   2297         listing_file.to_string_lossy().as_ref(),
   2298     ]);
   2299 
   2300     assert!(!output.status.success());
   2301     assert_eq!(value["operation_id"], "listing.update");
   2302     assert_eq!(value["result"], Value::Null);
   2303     assert_eq!(value["errors"][0]["code"], "network_unavailable");
   2304     assert_eq!(value["errors"][0]["detail"]["class"], "network");
   2305     assert_contains(
   2306         &value["errors"][0]["message"],
   2307         "SDK relay publish did not reach accepted quorum",
   2308     );
   2309     assert!(
   2310         !value["errors"][0]["message"]
   2311             .as_str()
   2312             .expect("error message")
   2313             .contains("not implemented")
   2314     );
   2315     assert_no_removed_command_reference(&value, &["listing", "update"]);
   2316     assert_no_daemon_runtime_reference(&value, &["listing", "update"]);
   2317 }
   2318 
   2319 #[test]
   2320 fn removed_order_submit_watch_flag_is_rejected_publicly() {
   2321     let output = radroots()
   2322         .args(["order", "submit", "--watch"])
   2323         .output()
   2324         .expect("run removed order submit watch flag");
   2325 
   2326     assert!(!output.status.success());
   2327     let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
   2328     assert!(stderr.contains("unexpected argument") || stderr.contains("unrecognized"));
   2329 }
   2330 
   2331 #[test]
   2332 fn removed_command_families_are_rejected_publicly() {
   2333     for command in [
   2334         "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "product",
   2335         "runtime", "job", "message", "approval", "agent",
   2336     ] {
   2337         let output = radroots()
   2338             .arg(command)
   2339             .output()
   2340             .expect("run removed command");
   2341 
   2342         assert!(!output.status.success(), "`{command}` should be rejected");
   2343         let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
   2344         assert!(stderr.contains("unrecognized subcommand"));
   2345     }
   2346 }
   2347 
   2348 #[test]
   2349 fn seller_order_decision_and_status_commands_are_public() {
   2350     for (operation_id, args) in [
   2351         (
   2352             "order.accept",
   2353             [
   2354                 "--format",
   2355                 "json",
   2356                 "--dry-run",
   2357                 "order",
   2358                 "accept",
   2359                 "ord_public",
   2360             ]
   2361             .as_slice(),
   2362         ),
   2363         (
   2364             "order.decline",
   2365             [
   2366                 "--format",
   2367                 "json",
   2368                 "--dry-run",
   2369                 "order",
   2370                 "decline",
   2371                 "ord_public",
   2372                 "--reason",
   2373                 "out_of_stock",
   2374             ]
   2375             .as_slice(),
   2376         ),
   2377         (
   2378             "order.cancel",
   2379             [
   2380                 "--format",
   2381                 "json",
   2382                 "--dry-run",
   2383                 "order",
   2384                 "cancel",
   2385                 "ord_public",
   2386                 "--reason",
   2387                 "changed plans",
   2388             ]
   2389             .as_slice(),
   2390         ),
   2391         (
   2392             "order.status.get",
   2393             ["--format", "json", "order", "status", "get", "ord_public"].as_slice(),
   2394         ),
   2395     ] {
   2396         let output = radroots()
   2397             .args(args)
   2398             .output()
   2399             .expect("run seller order command");
   2400         let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
   2401 
   2402         assert_eq!(value["operation_id"], operation_id);
   2403         assert_ne!(
   2404             String::from_utf8(output.stderr).expect("utf8 stderr"),
   2405             "unrecognized subcommand"
   2406         );
   2407     }
   2408 
   2409     let output = radroots()
   2410         .args(["order", "decision", "accept", "ord_deferred"])
   2411         .output()
   2412         .expect("run removed nested decision command");
   2413 
   2414     assert!(!output.status.success());
   2415     let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
   2416     assert!(stderr.contains("unrecognized subcommand"));
   2417 }
   2418 
   2419 #[test]
   2420 fn removed_order_post_agreement_subcommands_are_rejected_publicly() {
   2421     for args in [
   2422         ["order", "fulfillment", "update", "ord_public"].as_slice(),
   2423         ["order", "receipt", "record", "ord_public"].as_slice(),
   2424         ["order", "payment", "record", "ord_public"].as_slice(),
   2425         ["order", "settlement", "accept", "ord_public"].as_slice(),
   2426         ["order", "settlement", "reject", "ord_public"].as_slice(),
   2427     ] {
   2428         let output = radroots()
   2429             .args(args)
   2430             .output()
   2431             .expect("run removed order command");
   2432 
   2433         assert!(!output.status.success(), "`{args:?}` should be rejected");
   2434         let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
   2435         assert!(stderr.contains("unrecognized subcommand"));
   2436     }
   2437 }
   2438 
   2439 #[test]
   2440 fn target_outputs_do_not_suggest_removed_command_families() {
   2441     let sandbox = RadrootsCliSandbox::new();
   2442 
   2443     for args in [
   2444         ["--format", "json", "market", "product", "search", "eggs"].as_slice(),
   2445         ["--format", "json", "market", "listing", "get", "eggs"].as_slice(),
   2446         ["--format", "json", "listing", "get", "eggs"].as_slice(),
   2447         ["--format", "json", "listing", "list"].as_slice(),
   2448         [
   2449             "--format",
   2450             "json",
   2451             "order",
   2452             "get",
   2453             "ord_AAAAAAAAAAAAAAAAAAAAAA",
   2454         ]
   2455         .as_slice(),
   2456     ] {
   2457         let value = sandbox.json_success(args);
   2458         assert_no_removed_command_reference(&value, args);
   2459     }
   2460 
   2461     let sync_args = ["--format", "json", "sync", "status", "get"];
   2462     let value = sandbox.json_success(&sync_args);
   2463     assert_eq!(value["operation_id"], "sync.status.get");
   2464     assert_eq!(
   2465         value["result"]["source"],
   2466         "SDK canonical event store and outbox"
   2467     );
   2468     assert_no_removed_command_reference(&value, &sync_args);
   2469 }
   2470 
   2471 #[test]
   2472 fn listing_list_reports_empty_local_draft_state_truthfully() {
   2473     let sandbox = RadrootsCliSandbox::new();
   2474     let value = sandbox.json_success(&["--format", "json", "listing", "list"]);
   2475 
   2476     assert_eq!(value["operation_id"], "listing.list");
   2477     assert_eq!(value["result"]["state"], "empty");
   2478     assert_eq!(value["result"]["count"], 0);
   2479     assert_eq!(
   2480         value["result"]["listings"]
   2481             .as_array()
   2482             .expect("listings")
   2483             .len(),
   2484         0
   2485     );
   2486     assert!(
   2487         value["result"]["draft_dir"]
   2488             .as_str()
   2489             .expect("draft dir")
   2490             .ends_with("listings/drafts")
   2491     );
   2492     assert_no_removed_command_reference(&value, &["listing", "list"]);
   2493 }
   2494 
   2495 #[test]
   2496 fn listing_list_reports_default_local_drafts() {
   2497     let sandbox = RadrootsCliSandbox::new();
   2498     sandbox.json_success(&["--format", "json", "account", "create"]);
   2499     sandbox.json_success(&[
   2500         "--format",
   2501         "json",
   2502         "farm",
   2503         "create",
   2504         "--name",
   2505         "Green Farm",
   2506         "--location",
   2507         "farmstand",
   2508         "--country",
   2509         "US",
   2510         "--delivery-method",
   2511         "pickup",
   2512     ]);
   2513     let create = sandbox.json_success(&[
   2514         "--format",
   2515         "json",
   2516         "listing",
   2517         "create",
   2518         "--key",
   2519         "eggs",
   2520         "--title",
   2521         "Eggs",
   2522         "--category",
   2523         "eggs",
   2524         "--summary",
   2525         "Fresh eggs",
   2526         "--bin-id",
   2527         "bin-1",
   2528         "--quantity-amount",
   2529         "1",
   2530         "--quantity-unit",
   2531         "each",
   2532         "--price-amount",
   2533         "6",
   2534         "--price-currency",
   2535         "USD",
   2536         "--price-per-amount",
   2537         "1",
   2538         "--price-per-unit",
   2539         "each",
   2540         "--available",
   2541         "10",
   2542     ]);
   2543     let listing_file = create["result"]["file"].as_str().expect("listing file");
   2544     assert!(Path::new(listing_file).exists());
   2545 
   2546     let value = sandbox.json_success(&["--format", "json", "listing", "list"]);
   2547     let listing = &value["result"]["listings"][0];
   2548 
   2549     assert_eq!(value["operation_id"], "listing.list");
   2550     assert_eq!(value["result"]["state"], "ready");
   2551     assert_eq!(value["result"]["count"], 1);
   2552     assert_eq!(listing["id"], create["result"]["listing_id"]);
   2553     assert_eq!(listing["state"], "ready");
   2554     assert_eq!(listing["file"], listing_file);
   2555     assert_eq!(listing["product_key"], "eggs");
   2556     assert_eq!(listing["title"], "Eggs");
   2557     assert_eq!(listing["category"], "eggs");
   2558     assert_eq!(listing["location_primary"], "farmstand");
   2559     assert!(listing["seller_pubkey"].is_string());
   2560     assert!(listing["farm_d_tag"].is_string());
   2561     assert_no_removed_command_reference(&value, &["listing", "list"]);
   2562 }
   2563 
   2564 #[test]
   2565 fn listing_rebind_updates_seller_actor_with_approval() {
   2566     let sandbox = RadrootsCliSandbox::new();
   2567     let first = sandbox.json_success(&["--format", "json", "account", "create"]);
   2568     let first_account_id = first["result"]["account"]["id"]
   2569         .as_str()
   2570         .expect("first account id");
   2571     let listing_file = create_listing_draft(&sandbox, "rebind-listing");
   2572     make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
   2573     let initial_validation = sandbox.json_success(&[
   2574         "--format",
   2575         "json",
   2576         "listing",
   2577         "validate",
   2578         listing_file.to_string_lossy().as_ref(),
   2579     ]);
   2580     let first_pubkey = initial_validation["result"]["seller_pubkey"]
   2581         .as_str()
   2582         .expect("first pubkey");
   2583     let before = fs::read_to_string(&listing_file).expect("listing before");
   2584     let second = sandbox.json_success(&["--format", "json", "account", "create"]);
   2585     let second_account_id = second["result"]["account"]["id"]
   2586         .as_str()
   2587         .expect("second account id");
   2588 
   2589     let dry_run = sandbox.json_success(&[
   2590         "--format",
   2591         "json",
   2592         "--dry-run",
   2593         "listing",
   2594         "rebind",
   2595         listing_file.to_string_lossy().as_ref(),
   2596         second_account_id,
   2597         "--farm-d-tag",
   2598         "AAAAAAAAAAAAAAAAAAAAAw",
   2599     ]);
   2600     assert_eq!(dry_run["operation_id"], "listing.rebind");
   2601     assert_eq!(dry_run["result"]["state"], "dry_run");
   2602     assert_eq!(
   2603         dry_run["result"]["from_seller_account_id"],
   2604         first_account_id
   2605     );
   2606     assert_eq!(dry_run["result"]["from_seller_pubkey"], first_pubkey);
   2607     assert_eq!(dry_run["result"]["to_seller_account_id"], second_account_id);
   2608     let second_pubkey = dry_run["result"]["to_seller_pubkey"]
   2609         .as_str()
   2610         .expect("second pubkey");
   2611     assert_eq!(dry_run["result"]["seller_pubkey_changed"], true);
   2612     assert_eq!(
   2613         fs::read_to_string(&listing_file).expect("listing after dry-run"),
   2614         before
   2615     );
   2616 
   2617     let unapproved = sandbox.json_output(&[
   2618         "--format",
   2619         "json",
   2620         "listing",
   2621         "rebind",
   2622         listing_file.to_string_lossy().as_ref(),
   2623         second_account_id,
   2624         "--farm-d-tag",
   2625         "AAAAAAAAAAAAAAAAAAAAAw",
   2626     ]);
   2627     assert!(!unapproved.0.status.success());
   2628     assert_eq!(unapproved.1["errors"][0]["code"], "approval_required");
   2629 
   2630     let rebound = sandbox.json_success(&[
   2631         "--format",
   2632         "json",
   2633         "--approval-token",
   2634         "approve",
   2635         "listing",
   2636         "rebind",
   2637         listing_file.to_string_lossy().as_ref(),
   2638         second_account_id,
   2639         "--farm-d-tag",
   2640         "AAAAAAAAAAAAAAAAAAAAAw",
   2641     ]);
   2642     assert_eq!(rebound["operation_id"], "listing.rebind");
   2643     assert_eq!(rebound["result"]["state"], "rebound");
   2644     let after = fs::read_to_string(&listing_file).expect("listing after rebind");
   2645     assert!(after.contains("[seller_actor]"));
   2646     assert!(after.contains(second_account_id));
   2647     assert!(after.contains("source = \"listing_rebind\""));
   2648 
   2649     let validation = sandbox.json_success(&[
   2650         "--format",
   2651         "json",
   2652         "listing",
   2653         "validate",
   2654         listing_file.to_string_lossy().as_ref(),
   2655     ]);
   2656     assert_eq!(validation["result"]["valid"], true);
   2657     assert_eq!(validation["result"]["seller_account_id"], second_account_id);
   2658     assert_eq!(validation["result"]["seller_pubkey"], second_pubkey);
   2659 }
   2660 
   2661 #[test]
   2662 fn account_id_global_populates_envelope_actor() {
   2663     let output = radroots()
   2664         .args([
   2665             "--format",
   2666             "json",
   2667             "--account-id",
   2668             "acct_test",
   2669             "workspace",
   2670             "get",
   2671         ])
   2672         .output()
   2673         .expect("run workspace get");
   2674 
   2675     assert!(output.status.success());
   2676     let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
   2677 
   2678     assert_eq!(value["operation_id"], "workspace.get");
   2679     assert_eq!(value["actor"]["account_id"], "acct_test");
   2680     assert_eq!(value["actor"]["role"], "account");
   2681 }
   2682 
   2683 #[test]
   2684 fn target_command_outputs_standard_json_envelope() {
   2685     let output = radroots()
   2686         .args(["--format", "json", "workspace", "get"])
   2687         .output()
   2688         .expect("run workspace get");
   2689 
   2690     assert!(output.status.success());
   2691     assert!(output.stderr.is_empty());
   2692     let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
   2693 
   2694     assert_eq!(value["schema_version"], "radroots.cli.output.v1");
   2695     assert_eq!(value["operation_id"], "workspace.get");
   2696     assert_eq!(value["kind"], "workspace.get");
   2697     assert_eq!(value["dry_run"], false);
   2698     assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
   2699 }
   2700 
   2701 #[test]
   2702 fn next_actions_mirror_result_actions_for_json_and_ndjson() {
   2703     let sandbox = RadrootsCliSandbox::new();
   2704 
   2705     let value = sandbox.json_success(&["--format", "json", "market", "refresh"]);
   2706 
   2707     assert_eq!(value["result"]["actions"][0], "radroots store init");
   2708     assert_eq!(value["next_actions"][0]["label"], "store init");
   2709     assert_eq!(value["next_actions"][0]["command"], "radroots store init");
   2710 
   2711     let output = sandbox
   2712         .command()
   2713         .args(["--format", "ndjson", "market", "refresh"])
   2714         .output()
   2715         .expect("run market refresh ndjson");
   2716     let frames = ndjson_from_stdout(&output);
   2717     let terminal = frames.last().expect("terminal ndjson frame");
   2718 
   2719     assert!(output.status.success());
   2720     assert_eq!(
   2721         terminal["payload"]["next_actions"][0]["command"],
   2722         "radroots store init"
   2723     );
   2724 
   2725     for args in [
   2726         &["--format", "ndjson", "config", "get"][..],
   2727         &["--format", "ndjson", "health", "status", "get"][..],
   2728         &["--format", "ndjson", "health", "check", "run"][..],
   2729     ] {
   2730         let daemon = RadrootsCliSandbox::new();
   2731         daemon.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
   2732         let output = daemon.command().args(args).output().expect("run ndjson");
   2733         let frames = ndjson_from_stdout(&output);
   2734         let terminal = frames.last().expect("terminal ndjson frame");
   2735 
   2736         assert!(output.status.success(), "{args:?}");
   2737         assert!(
   2738             terminal["payload"]["next_actions"]
   2739                 .as_array()
   2740                 .expect("next actions")
   2741                 .iter()
   2742                 .any(|action| action["description"]
   2743                     == "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"),
   2744             "{args:?}"
   2745         );
   2746     }
   2747 }
   2748 
   2749 #[test]
   2750 fn default_human_output_is_concise_and_not_json() {
   2751     let output = radroots()
   2752         .args(["workspace", "get"])
   2753         .output()
   2754         .expect("run workspace get");
   2755 
   2756     assert!(output.status.success());
   2757     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   2758 
   2759     assert!(stdout.starts_with("workspace.get: ok\n"));
   2760     assert!(stdout.contains("request_id: req_workspace_get_"));
   2761     assert!(serde_json::from_str::<Value>(&stdout).is_err());
   2762 }
   2763 
   2764 #[test]
   2765 fn human_health_status_surfaces_publish_reason_and_actions() {
   2766     let sandbox = RadrootsCliSandbox::new();
   2767 
   2768     let output = sandbox
   2769         .command()
   2770         .args(["--relay", "ws://127.0.0.1:19007", "health", "status", "get"])
   2771         .output()
   2772         .expect("run human health status");
   2773 
   2774     assert!(output.status.success());
   2775     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   2776 
   2777     assert!(stdout.starts_with("health.status.get: needs_attention\n"));
   2778     assert!(stdout.contains("publish_state: unconfigured"));
   2779     assert!(stdout.contains("reason: direct_nostr_relay publish transport requires a selected or default write-capable local account for signed writes"));
   2780     assert!(stdout.contains("- radroots account create"));
   2781     assert!(serde_json::from_str::<Value>(&stdout).is_err());
   2782 }
   2783 
   2784 #[test]
   2785 fn human_market_refresh_missing_store_shows_action() {
   2786     let sandbox = RadrootsCliSandbox::new();
   2787 
   2788     let output = sandbox
   2789         .command()
   2790         .args(["market", "refresh"])
   2791         .output()
   2792         .expect("run human market refresh");
   2793 
   2794     assert!(output.status.success());
   2795     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   2796 
   2797     assert!(stdout.starts_with("market.refresh: unconfigured\n"));
   2798     assert!(stdout.contains("reason: local replica database is not initialized"));
   2799     assert!(stdout.contains("- radroots store init"));
   2800     assert!(serde_json::from_str::<Value>(&stdout).is_err());
   2801 }
   2802 
   2803 #[test]
   2804 fn human_failure_output_preserves_error_code_and_message() {
   2805     let output = radroots()
   2806         .args(["--format", "human", "order", "submit"])
   2807         .output()
   2808         .expect("run order submit");
   2809 
   2810     assert_eq!(output.status.code(), Some(6));
   2811     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   2812 
   2813     assert!(stdout.starts_with("order.submit: error\n"));
   2814     assert!(stdout.contains("request_id: req_order_submit_"));
   2815     assert!(stdout.contains("error: approval_required"));
   2816     assert!(stdout.contains("message: missing required `approval_token` input"));
   2817     assert!(serde_json::from_str::<Value>(&stdout).is_err());
   2818 }
   2819 
   2820 #[test]
   2821 fn human_failure_output_renders_structured_error_detail() {
   2822     let output = radroots()
   2823         .args([
   2824             "--format",
   2825             "human",
   2826             "order",
   2827             "event",
   2828             "watch",
   2829             "ord_missing",
   2830         ])
   2831         .output()
   2832         .expect("run order event watch");
   2833 
   2834     assert_eq!(output.status.code(), Some(3));
   2835     let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
   2836 
   2837     assert!(stdout.starts_with("order.event.watch: error\n"));
   2838     assert!(stdout.contains("request_id: req_order_event_watch_"));
   2839     assert!(stdout.contains("error: not_implemented"));
   2840     assert!(stdout.contains("state: not_implemented"));
   2841     assert!(stdout.contains("reason: relay-backed order event watch is not implemented"));
   2842     assert!(stdout.contains("- radroots order status get ord_missing"));
   2843     assert!(serde_json::from_str::<Value>(&stdout).is_err());
   2844 }
   2845 
   2846 #[test]
   2847 fn request_ids_are_invocation_unique_and_preserve_caller_fields() {
   2848     let first = radroots()
   2849         .args([
   2850             "--format",
   2851             "json",
   2852             "--correlation-id",
   2853             "corr_test",
   2854             "--idempotency-key",
   2855             "idem_test",
   2856             "workspace",
   2857             "get",
   2858         ])
   2859         .output()
   2860         .expect("run first workspace get");
   2861     let second = radroots()
   2862         .args([
   2863             "--format",
   2864             "json",
   2865             "--correlation-id",
   2866             "corr_test",
   2867             "--idempotency-key",
   2868             "idem_test",
   2869             "workspace",
   2870             "get",
   2871         ])
   2872         .output()
   2873         .expect("run second workspace get");
   2874 
   2875     assert!(first.status.success());
   2876     assert!(second.status.success());
   2877     let first: Value = serde_json::from_slice(&first.stdout).expect("first json envelope");
   2878     let second: Value = serde_json::from_slice(&second.stdout).expect("second json envelope");
   2879 
   2880     assert_eq!(first["correlation_id"], "corr_test");
   2881     assert_eq!(first["idempotency_key"], "idem_test");
   2882     assert_eq!(second["correlation_id"], "corr_test");
   2883     assert_eq!(second["idempotency_key"], "idem_test");
   2884     assert!(
   2885         first["request_id"]
   2886             .as_str()
   2887             .expect("first request id")
   2888             .starts_with("req_workspace_get_")
   2889     );
   2890     assert_ne!(first["request_id"], second["request_id"]);
   2891 }
   2892 
   2893 #[test]
   2894 fn supported_ndjson_outputs_started_and_completed_frames() {
   2895     let sandbox = RadrootsCliSandbox::new();
   2896     let output = sandbox
   2897         .command()
   2898         .args(["--format", "ndjson", "account", "list"])
   2899         .output()
   2900         .expect("run account list ndjson");
   2901 
   2902     assert!(output.status.success());
   2903     let frames = ndjson_from_stdout(&output);
   2904 
   2905     assert_eq!(frames.len(), 2);
   2906     assert_eq!(frames[0]["schema_version"], "radroots.cli.output.v1");
   2907     assert_eq!(frames[0]["operation_id"], "account.list");
   2908     assert_eq!(frames[0]["frame_type"], "started");
   2909     assert_eq!(frames[0]["sequence"], 0);
   2910     assert_eq!(frames[1]["operation_id"], "account.list");
   2911     assert_eq!(frames[1]["frame_type"], "completed");
   2912     assert_eq!(frames[1]["sequence"], 1);
   2913     assert_eq!(frames[1]["errors"].as_array().expect("errors").len(), 0);
   2914     assert_eq!(frames[0]["request_id"], frames[1]["request_id"]);
   2915 }
   2916 
   2917 #[test]
   2918 fn unsupported_ndjson_returns_structured_invalid_input() {
   2919     let output = radroots()
   2920         .args(["--format", "ndjson", "workspace", "get"])
   2921         .output()
   2922         .expect("run workspace get ndjson");
   2923 
   2924     assert_eq!(output.status.code(), Some(2));
   2925     let frames = ndjson_from_stdout(&output);
   2926 
   2927     assert_eq!(frames.len(), 2);
   2928     assert_eq!(frames[0]["operation_id"], "workspace.get");
   2929     assert_eq!(frames[0]["frame_type"], "started");
   2930     assert_eq!(frames[1]["operation_id"], "workspace.get");
   2931     assert_eq!(frames[1]["frame_type"], "error");
   2932     assert_eq!(frames[1]["errors"][0]["code"], "invalid_input");
   2933     assert_eq!(frames[1]["errors"][0]["exit_code"], 2);
   2934 
   2935     let watch_output = radroots()
   2936         .args([
   2937             "--format",
   2938             "ndjson",
   2939             "order",
   2940             "event",
   2941             "watch",
   2942             "ord_missing",
   2943         ])
   2944         .output()
   2945         .expect("run order event watch ndjson");
   2946 
   2947     assert_eq!(watch_output.status.code(), Some(2));
   2948     let watch_frames = ndjson_from_stdout(&watch_output);
   2949     assert_eq!(watch_frames.len(), 2);
   2950     assert_eq!(watch_frames[0]["operation_id"], "order.event.watch");
   2951     assert_eq!(watch_frames[0]["frame_type"], "started");
   2952     assert_eq!(watch_frames[1]["operation_id"], "order.event.watch");
   2953     assert_eq!(watch_frames[1]["frame_type"], "error");
   2954     assert_eq!(watch_frames[1]["errors"][0]["code"], "invalid_input");
   2955     assert_eq!(watch_frames[1]["errors"][0]["exit_code"], 2);
   2956 }
   2957 
   2958 #[test]
   2959 fn machine_output_exposes_status_format_resource_and_reason_code() {
   2960     let sandbox = RadrootsCliSandbox::new();
   2961 
   2962     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   2963     assert_eq!(account["status"], "ok");
   2964     assert_eq!(account["output_format"], "json");
   2965     assert_eq!(account["reason_code"], Value::Null);
   2966     assert_eq!(account["resource"]["kind"], "account");
   2967     assert_eq!(
   2968         account["resource"]["id"],
   2969         account["result"]["account"]["id"]
   2970     );
   2971 
   2972     let output = sandbox
   2973         .command()
   2974         .args(["--format", "json", "--dry-run", "workspace", "get"])
   2975         .output()
   2976         .expect("run invalid dry-run");
   2977     assert_eq!(output.status.code(), Some(2));
   2978     let invalid = json_from_stdout(&output);
   2979     assert_eq!(invalid["status"], "error");
   2980     assert_eq!(invalid["reason_code"], "invalid_input");
   2981     assert_eq!(invalid["errors"][0]["reason_code"], "invalid_input");
   2982 
   2983     let ndjson_output = sandbox
   2984         .command()
   2985         .args(["--format", "ndjson", "--dry-run", "workspace", "get"])
   2986         .output()
   2987         .expect("run invalid ndjson");
   2988     assert_eq!(ndjson_output.status.code(), Some(2));
   2989     let frames = ndjson_from_stdout(&ndjson_output);
   2990     assert_eq!(frames[0]["payload"]["status"], "error");
   2991     assert_eq!(frames[0]["payload"]["output_format"], "ndjson");
   2992     assert_eq!(frames[1]["payload"]["status"], "error");
   2993     assert_eq!(frames[1]["payload"]["output_format"], "ndjson");
   2994     assert_eq!(frames[1]["payload"]["reason_code"], "invalid_input");
   2995     assert_eq!(frames[1]["errors"][0]["reason_code"], "invalid_input");
   2996 }
   2997 
   2998 #[test]
   2999 fn offline_forbids_external_network_operations() {
   3000     for (operation_id, args) in [
   3001         (
   3002             "sync.pull",
   3003             ["--format", "json", "--offline", "sync", "pull"].as_slice(),
   3004         ),
   3005         (
   3006             "sync.push",
   3007             ["--format", "json", "--offline", "sync", "push"].as_slice(),
   3008         ),
   3009         (
   3010             "market.refresh",
   3011             ["--format", "json", "--offline", "market", "refresh"].as_slice(),
   3012         ),
   3013         (
   3014             "order.submit",
   3015             ["--format", "json", "--offline", "order", "submit"].as_slice(),
   3016         ),
   3017         (
   3018             "order.cancel",
   3019             [
   3020                 "--format",
   3021                 "json",
   3022                 "--offline",
   3023                 "order",
   3024                 "cancel",
   3025                 "ord_offline_cancel",
   3026                 "--reason",
   3027                 "changed plans",
   3028             ]
   3029             .as_slice(),
   3030         ),
   3031         (
   3032             "order.revision.propose",
   3033             [
   3034                 "--format",
   3035                 "json",
   3036                 "--offline",
   3037                 "--approval-token",
   3038                 "approve",
   3039                 "order",
   3040                 "revision",
   3041                 "propose",
   3042                 "ord_offline_revision",
   3043                 "--reason",
   3044                 "update count",
   3045                 "--bin-id",
   3046                 "bin-1",
   3047                 "--bin-count",
   3048                 "2",
   3049             ]
   3050             .as_slice(),
   3051         ),
   3052         (
   3053             "order.revision.accept",
   3054             [
   3055                 "--format",
   3056                 "json",
   3057                 "--offline",
   3058                 "--approval-token",
   3059                 "approve",
   3060                 "order",
   3061                 "revision",
   3062                 "accept",
   3063                 "ord_offline_revision",
   3064                 "--revision-id",
   3065                 "revision_1",
   3066             ]
   3067             .as_slice(),
   3068         ),
   3069         (
   3070             "order.revision.decline",
   3071             [
   3072                 "--format",
   3073                 "json",
   3074                 "--offline",
   3075                 "--approval-token",
   3076                 "approve",
   3077                 "order",
   3078                 "revision",
   3079                 "decline",
   3080                 "ord_offline_revision",
   3081                 "--revision-id",
   3082                 "revision_1",
   3083                 "--reason",
   3084                 "keep original",
   3085             ]
   3086             .as_slice(),
   3087         ),
   3088     ] {
   3089         let output = radroots()
   3090             .args(args)
   3091             .output()
   3092             .expect("run offline external command");
   3093 
   3094         assert!(!output.status.success());
   3095         let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
   3096 
   3097         assert_eq!(value["operation_id"], operation_id);
   3098         assert_eq!(value["result"], Value::Null);
   3099         assert_eq!(value["errors"][0]["code"], "offline_forbidden");
   3100         assert_eq!(value["errors"][0]["exit_code"], 8);
   3101     }
   3102 }
   3103 
   3104 #[test]
   3105 fn offline_allows_supported_external_dry_run() {
   3106     let sandbox = RadrootsCliSandbox::new();
   3107     sandbox.json_success(&["--format", "json", "account", "create"]);
   3108     let listing_file = create_listing_draft(&sandbox, "offline-dry-run");
   3109     make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
   3110 
   3111     let publish = sandbox.json_success(&[
   3112         "--format",
   3113         "json",
   3114         "--offline",
   3115         "--dry-run",
   3116         "listing",
   3117         "publish",
   3118         listing_file.to_string_lossy().as_ref(),
   3119     ]);
   3120 
   3121     assert_eq!(publish["operation_id"], "listing.publish");
   3122     assert_eq!(publish["result"]["state"], "dry_run");
   3123 
   3124     sandbox.json_success(&["--format", "json", "store", "init"]);
   3125     let sync_push = sandbox.json_success(&[
   3126         "--format",
   3127         "json",
   3128         "--offline",
   3129         "--relay",
   3130         "ws://127.0.0.1:9",
   3131         "--dry-run",
   3132         "sync",
   3133         "push",
   3134     ]);
   3135 
   3136     assert_eq!(sync_push["operation_id"], "sync.push");
   3137     assert_eq!(sync_push["result"]["state"], "ready");
   3138 }
   3139 
   3140 #[test]
   3141 fn offline_listing_publish_enqueues_sdk_outbox_without_direct_relay_push() {
   3142     let sandbox = RadrootsCliSandbox::new();
   3143     sandbox.json_success(&["--format", "json", "account", "create"]);
   3144     let farm = sandbox.json_success(&[
   3145         "--format",
   3146         "json",
   3147         "farm",
   3148         "create",
   3149         "--name",
   3150         "Offline Farm",
   3151         "--location",
   3152         "farmstand",
   3153         "--country",
   3154         "US",
   3155         "--delivery-method",
   3156         "pickup",
   3157     ]);
   3158     let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
   3159         .as_str()
   3160         .expect("farm d tag");
   3161     let listing_file = create_listing_draft(&sandbox, "offline-sdk-enqueue");
   3162     make_listing_publishable(&listing_file, farm_d_tag);
   3163     let relay = "ws://127.0.0.1:9";
   3164     let local_event_records_before_publish = sandbox.local_event_records().len();
   3165 
   3166     let publish = sandbox.json_success(&[
   3167         "--format",
   3168         "json",
   3169         "--offline",
   3170         "--relay",
   3171         relay,
   3172         "--approval-token",
   3173         "approve",
   3174         "listing",
   3175         "publish",
   3176         listing_file.to_string_lossy().as_ref(),
   3177     ]);
   3178 
   3179     assert_eq!(publish["operation_id"], "listing.publish");
   3180     assert_eq!(publish["result"]["state"], "queued");
   3181     assert_eq!(
   3182         publish["result"]["source"],
   3183         "SDK listing publish ยท configured signer"
   3184     );
   3185     assert_eq!(publish["result"]["target_relays"][0], relay);
   3186     assert_eq!(publish["result"]["actions"][0], "radroots sync push");
   3187     assert_eq!(
   3188         publish["result"]["event_id"]
   3189             .as_str()
   3190             .expect("sdk event id")
   3191             .len(),
   3192         64
   3193     );
   3194     assert!(
   3195         sandbox
   3196             .root()
   3197             .join("data/apps/cli/replica/sdk/outbox.sqlite")
   3198             .exists()
   3199     );
   3200     assert_eq!(
   3201         sandbox.local_event_records().len(),
   3202         local_event_records_before_publish
   3203     );
   3204 }
   3205 
   3206 #[test]
   3207 fn listing_publish_idempotency_conflict_maps_sdk_partial_mutation_recovery() {
   3208     let sandbox = RadrootsCliSandbox::new();
   3209     sandbox.json_success(&["--format", "json", "account", "create"]);
   3210     let farm = sandbox.json_success(&[
   3211         "--format",
   3212         "json",
   3213         "farm",
   3214         "create",
   3215         "--name",
   3216         "Conflict Farm",
   3217         "--location",
   3218         "farmstand",
   3219         "--country",
   3220         "US",
   3221         "--delivery-method",
   3222         "pickup",
   3223     ]);
   3224     let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
   3225         .as_str()
   3226         .expect("farm d tag");
   3227     let listing_file = create_listing_draft(&sandbox, "idem-conflict");
   3228     make_listing_publishable(&listing_file, farm_d_tag);
   3229     let relay = "ws://127.0.0.1:9";
   3230     let idempotency_key = "listing-idem-conflict";
   3231 
   3232     sandbox.json_success(&[
   3233         "--format",
   3234         "json",
   3235         "--offline",
   3236         "--relay",
   3237         relay,
   3238         "--approval-token",
   3239         "approve",
   3240         "--idempotency-key",
   3241         idempotency_key,
   3242         "listing",
   3243         "publish",
   3244         listing_file.to_string_lossy().as_ref(),
   3245     ]);
   3246     let raw = fs::read_to_string(&listing_file).expect("listing draft");
   3247     fs::write(
   3248         &listing_file,
   3249         raw.replace("title = \"Eggs\"", "title = \"Conflict Eggs\""),
   3250     )
   3251     .expect("rewrite listing draft");
   3252 
   3253     let (output, conflict) = sandbox.json_output(&[
   3254         "--format",
   3255         "json",
   3256         "--offline",
   3257         "--relay",
   3258         relay,
   3259         "--approval-token",
   3260         "approve",
   3261         "--idempotency-key",
   3262         idempotency_key,
   3263         "listing",
   3264         "publish",
   3265         listing_file.to_string_lossy().as_ref(),
   3266     ]);
   3267 
   3268     assert!(!output.status.success());
   3269     assert_eq!(conflict["operation_id"], "listing.publish");
   3270     assert_eq!(conflict["errors"][0]["code"], "partial_local_mutation");
   3271     assert_eq!(conflict["errors"][0]["detail"]["class"], "local_mutation");
   3272     assert_eq!(
   3273         conflict["errors"][0]["detail"]["detail"]["failure"],
   3274         "outbox_idempotency_conflict"
   3275     );
   3276     assert_eq!(
   3277         conflict["errors"][0]["detail"]["actions"][0],
   3278         "radroots listing publish"
   3279     );
   3280 }
   3281 
   3282 #[test]
   3283 fn offline_rejects_order_decision_dry_run() {
   3284     for (operation_id, args) in [
   3285         (
   3286             "order.accept",
   3287             [
   3288                 "--format",
   3289                 "json",
   3290                 "--offline",
   3291                 "--dry-run",
   3292                 "order",
   3293                 "accept",
   3294                 "ord_offline_decision",
   3295             ]
   3296             .as_slice(),
   3297         ),
   3298         (
   3299             "order.decline",
   3300             [
   3301                 "--format",
   3302                 "json",
   3303                 "--offline",
   3304                 "--dry-run",
   3305                 "order",
   3306                 "decline",
   3307                 "ord_offline_decision",
   3308                 "--reason",
   3309                 "unavailable",
   3310             ]
   3311             .as_slice(),
   3312         ),
   3313         (
   3314             "order.cancel",
   3315             [
   3316                 "--format",
   3317                 "json",
   3318                 "--offline",
   3319                 "--dry-run",
   3320                 "order",
   3321                 "cancel",
   3322                 "ord_offline_decision",
   3323                 "--reason",
   3324                 "changed plans",
   3325             ]
   3326             .as_slice(),
   3327         ),
   3328         (
   3329             "order.revision.propose",
   3330             [
   3331                 "--format",
   3332                 "json",
   3333                 "--offline",
   3334                 "--dry-run",
   3335                 "order",
   3336                 "revision",
   3337                 "propose",
   3338                 "ord_offline_revision",
   3339                 "--reason",
   3340                 "update count",
   3341                 "--bin-id",
   3342                 "bin-1",
   3343                 "--bin-count",
   3344                 "2",
   3345             ]
   3346             .as_slice(),
   3347         ),
   3348         (
   3349             "order.revision.accept",
   3350             [
   3351                 "--format",
   3352                 "json",
   3353                 "--offline",
   3354                 "--dry-run",
   3355                 "order",
   3356                 "revision",
   3357                 "accept",
   3358                 "ord_offline_revision",
   3359                 "--revision-id",
   3360                 "revision_1",
   3361             ]
   3362             .as_slice(),
   3363         ),
   3364         (
   3365             "order.revision.decline",
   3366             [
   3367                 "--format",
   3368                 "json",
   3369                 "--offline",
   3370                 "--dry-run",
   3371                 "order",
   3372                 "revision",
   3373                 "decline",
   3374                 "ord_offline_revision",
   3375                 "--revision-id",
   3376                 "revision_1",
   3377                 "--reason",
   3378                 "keep original",
   3379             ]
   3380             .as_slice(),
   3381         ),
   3382     ] {
   3383         let output = radroots()
   3384             .args(args)
   3385             .output()
   3386             .expect("run offline order decision dry-run");
   3387         let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
   3388 
   3389         assert_eq!(output.status.code(), Some(8));
   3390         assert_eq!(value["operation_id"], operation_id);
   3391         assert_eq!(value["dry_run"], true);
   3392         assert_eq!(value["result"], Value::Null);
   3393         assert_eq!(value["errors"][0]["code"], "offline_forbidden");
   3394         assert_eq!(value["errors"][0]["exit_code"], 8);
   3395     }
   3396 }
   3397 
   3398 #[test]
   3399 fn listing_publish_dry_run_validates_missing_file() {
   3400     let sandbox = RadrootsCliSandbox::new();
   3401     let missing = sandbox.root().join("missing-listing.toml");
   3402     let (output, value) = sandbox.json_output(&[
   3403         "--format",
   3404         "json",
   3405         "--dry-run",
   3406         "listing",
   3407         "publish",
   3408         missing.to_string_lossy().as_ref(),
   3409     ]);
   3410 
   3411     assert!(!output.status.success());
   3412     assert_eq!(value["operation_id"], "listing.publish");
   3413     assert_eq!(value["result"], Value::Null);
   3414     assert_eq!(value["errors"][0]["code"], "not_found");
   3415     assert_eq!(value["errors"][0]["exit_code"], 4);
   3416     assert_no_removed_command_reference(
   3417         &value,
   3418         &["listing", "publish", "--dry-run", "missing-listing.toml"],
   3419     );
   3420 }
   3421 
   3422 #[test]
   3423 fn listing_publish_invalid_draft_returns_validation_failure() {
   3424     let sandbox = RadrootsCliSandbox::new();
   3425     let invalid = sandbox.root().join("invalid-listing.toml");
   3426     fs::write(&invalid, "listing = [").expect("write invalid listing");
   3427 
   3428     let (output, value) = sandbox.json_output(&[
   3429         "--format",
   3430         "json",
   3431         "--dry-run",
   3432         "listing",
   3433         "publish",
   3434         invalid.to_string_lossy().as_ref(),
   3435     ]);
   3436 
   3437     assert!(!output.status.success());
   3438     assert_eq!(value["operation_id"], "listing.publish");
   3439     assert_eq!(value["result"], Value::Null);
   3440     assert_eq!(value["errors"][0]["code"], "validation_failed");
   3441     assert_eq!(value["errors"][0]["exit_code"], 10);
   3442 }
   3443 
   3444 #[test]
   3445 fn online_requires_relay_for_external_network_operations() {
   3446     for (operation_id, args) in [
   3447         (
   3448             "sync.pull",
   3449             ["--format", "json", "--online", "sync", "pull"].as_slice(),
   3450         ),
   3451         (
   3452             "sync.push",
   3453             ["--format", "json", "--online", "sync", "push"].as_slice(),
   3454         ),
   3455         (
   3456             "market.refresh",
   3457             ["--format", "json", "--online", "market", "refresh"].as_slice(),
   3458         ),
   3459         (
   3460             "order.event.list",
   3461             ["--format", "json", "--online", "order", "event", "list"].as_slice(),
   3462         ),
   3463         (
   3464             "order.cancel",
   3465             [
   3466                 "--format",
   3467                 "json",
   3468                 "--online",
   3469                 "order",
   3470                 "cancel",
   3471                 "ord_missing",
   3472                 "--reason",
   3473                 "changed plans",
   3474             ]
   3475             .as_slice(),
   3476         ),
   3477         (
   3478             "order.revision.propose",
   3479             [
   3480                 "--format",
   3481                 "json",
   3482                 "--online",
   3483                 "--approval-token",
   3484                 "approve",
   3485                 "order",
   3486                 "revision",
   3487                 "propose",
   3488                 "ord_missing",
   3489                 "--reason",
   3490                 "update count",
   3491                 "--bin-id",
   3492                 "bin-1",
   3493                 "--bin-count",
   3494                 "2",
   3495             ]
   3496             .as_slice(),
   3497         ),
   3498         (
   3499             "order.revision.accept",
   3500             [
   3501                 "--format",
   3502                 "json",
   3503                 "--online",
   3504                 "--dry-run",
   3505                 "order",
   3506                 "revision",
   3507                 "accept",
   3508                 "ord_missing",
   3509                 "--revision-id",
   3510                 "revision_1",
   3511             ]
   3512             .as_slice(),
   3513         ),
   3514         (
   3515             "order.revision.decline",
   3516             [
   3517                 "--format",
   3518                 "json",
   3519                 "--online",
   3520                 "--dry-run",
   3521                 "order",
   3522                 "revision",
   3523                 "decline",
   3524                 "ord_missing",
   3525                 "--revision-id",
   3526                 "revision_1",
   3527                 "--reason",
   3528                 "keep original",
   3529             ]
   3530             .as_slice(),
   3531         ),
   3532     ] {
   3533         let output = radroots()
   3534             .args(args)
   3535             .output()
   3536             .expect("run online external command");
   3537 
   3538         assert!(!output.status.success());
   3539         let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
   3540 
   3541         assert_eq!(value["operation_id"], operation_id);
   3542         assert_eq!(value["result"], Value::Null);
   3543         assert_eq!(value["errors"][0]["code"], "network_unavailable");
   3544         assert_eq!(value["errors"][0]["exit_code"], 8);
   3545         assert!(
   3546             value["errors"][0]["message"]
   3547                 .as_str()
   3548                 .expect("message")
   3549                 .contains("requires at least one configured relay")
   3550         );
   3551     }
   3552 }
   3553 
   3554 #[test]
   3555 fn order_status_get_uses_sdk_local_projection_without_relay_fetch() {
   3556     let sandbox = RadrootsCliSandbox::new();
   3557     let local = sandbox.json_success(&[
   3558         "--format",
   3559         "json",
   3560         "--online",
   3561         "order",
   3562         "status",
   3563         "get",
   3564         "ord_missing",
   3565     ]);
   3566 
   3567     assert_eq!(local["operation_id"], "order.status.get");
   3568     assert_eq!(local["result"]["state"], "missing");
   3569     assert_eq!(local["result"]["source"], "SDK local order projection");
   3570     assert_eq!(
   3571         local["result"]["actor_context_source"],
   3572         "sdk_local_projection"
   3573     );
   3574     assert_eq!(local["result"]["fetched_count"], 0);
   3575     assert_eq!(local["result"]["decoded_count"], 0);
   3576 
   3577     let listener = TcpListener::bind("127.0.0.1:0").expect("bind closed relay");
   3578     let closed_relay = format!("ws://{}", listener.local_addr().expect("relay addr"));
   3579     drop(listener);
   3580     let with_closed_relay = sandbox.json_success(&[
   3581         "--format",
   3582         "json",
   3583         "--relay",
   3584         closed_relay.as_str(),
   3585         "order",
   3586         "status",
   3587         "get",
   3588         "ord_missing",
   3589     ]);
   3590 
   3591     assert_eq!(with_closed_relay["operation_id"], "order.status.get");
   3592     assert_eq!(with_closed_relay["result"]["state"], "missing");
   3593     assert_eq!(
   3594         with_closed_relay["result"]["source"],
   3595         "SDK local order projection"
   3596     );
   3597     assert_eq!(with_closed_relay["result"]["fetched_count"], 0);
   3598     assert_eq!(with_closed_relay["result"]["decoded_count"], 0);
   3599 }
   3600 
   3601 #[test]
   3602 fn order_status_get_invalid_order_id_uses_sdk_error_contract() {
   3603     let sandbox = RadrootsCliSandbox::new();
   3604     let (output, value) =
   3605         sandbox.json_output(&["--format", "json", "order", "status", "get", "bad order id"]);
   3606 
   3607     assert!(!output.status.success());
   3608     assert_eq!(value["operation_id"], "order.status.get");
   3609     assert_eq!(value["result"], Value::Null);
   3610     assert_eq!(value["errors"][0]["code"], "invalid_order_id");
   3611     assert_eq!(value["errors"][0]["exit_code"], 2);
   3612     assert_eq!(value["errors"][0]["detail"]["class"], "request");
   3613     assert_eq!(value["errors"][0]["detail"]["retryable"], false);
   3614     assert_eq!(
   3615         value["errors"][0]["detail"]["detail"]["value"],
   3616         "bad order id"
   3617     );
   3618 }
   3619 
   3620 #[test]
   3621 fn legacy_radrootsd_publish_transport_value_is_rejected() {
   3622     let sandbox = RadrootsCliSandbox::new();
   3623     let output = sandbox
   3624         .command()
   3625         .args(["--publish-transport", "radrootsd", "sync", "push"])
   3626         .output()
   3627         .expect("run legacy publish transport");
   3628     let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
   3629 
   3630     assert!(!output.status.success());
   3631     assert!(stderr.contains("invalid value"));
   3632     assert!(stderr.contains("radrootsd_proxy"));
   3633 }
   3634 
   3635 #[test]
   3636 fn online_order_event_watch_returns_deferred_without_relay_preflight() {
   3637     let sandbox = RadrootsCliSandbox::new();
   3638     let (output, value) = sandbox.json_output(&[
   3639         "--format",
   3640         "json",
   3641         "--online",
   3642         "order",
   3643         "event",
   3644         "watch",
   3645         "ord_missing",
   3646     ]);
   3647 
   3648     assert!(!output.status.success());
   3649     assert_eq!(output.status.code(), Some(3));
   3650     assert_eq!(value["operation_id"], "order.event.watch");
   3651     assert_eq!(value["result"], Value::Null);
   3652     assert_eq!(value["errors"][0]["code"], "not_implemented");
   3653     assert_eq!(value["errors"][0]["detail"]["state"], "not_implemented");
   3654     assert_eq!(value["errors"][0]["detail"]["order_id"], "ord_missing");
   3655     assert_eq!(
   3656         value["next_actions"][0]["command"],
   3657         "radroots order status get ord_missing"
   3658     );
   3659     assert!(
   3660         !value["errors"][0]["message"]
   3661             .as_str()
   3662             .expect("message")
   3663             .contains("configured relay")
   3664     );
   3665     assert_no_daemon_runtime_reference(&value, &["order", "event", "watch"]);
   3666 }
   3667 
   3668 #[test]
   3669 fn online_allows_local_diagnostics() {
   3670     let value = RadrootsCliSandbox::new().json_success(&[
   3671         "--format",
   3672         "json",
   3673         "--online",
   3674         "workspace",
   3675         "get",
   3676     ]);
   3677 
   3678     assert_eq!(value["operation_id"], "workspace.get");
   3679     assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
   3680 }
   3681 
   3682 #[test]
   3683 fn store_export_dry_run_is_structured_unsupported() {
   3684     let sandbox = RadrootsCliSandbox::new();
   3685     let (output, value) =
   3686         sandbox.json_output(&["--format", "json", "--dry-run", "store", "export"]);
   3687 
   3688     assert!(!output.status.success());
   3689     assert_eq!(output.status.code(), Some(2));
   3690     assert_eq!(value["operation_id"], "store.export");
   3691     assert_eq!(value["errors"][0]["code"], "invalid_input");
   3692     assert_eq!(value["errors"][0]["exit_code"], 2);
   3693 }
   3694 
   3695 #[test]
   3696 fn store_backup_and_restore_use_sdk_canonical_store() {
   3697     let sandbox = RadrootsCliSandbox::new();
   3698     let sdk_root = sandbox.root().join("data/apps/cli/replica/sdk");
   3699 
   3700     assert!(!sdk_root.exists());
   3701 
   3702     let status = sandbox.json_success(&["--format", "json", "store", "status", "get"]);
   3703 
   3704     assert_eq!(status["operation_id"], "store.status.get");
   3705     assert_eq!(status["result"]["state"], "ready");
   3706     assert_eq!(
   3707         status["result"]["source"],
   3708         "SDK canonical event store and outbox"
   3709     );
   3710     assert_eq!(status["result"]["canonical_store"], "sdk");
   3711     assert_eq!(status["result"]["sdk_storage"], "directory");
   3712     assert_eq!(status["result"]["sdk_existed_before_open"], false);
   3713     assert_eq!(
   3714         status["result"]["event_store"]["store"]["integrity_ok"],
   3715         true
   3716     );
   3717     assert_eq!(status["result"]["outbox"]["store"]["integrity_ok"], true);
   3718     assert_eq!(status["result"]["legacy_replica"]["state"], "unconfigured");
   3719     assert_eq!(
   3720         status["result"]["legacy_replica"]["source"],
   3721         "legacy local replica ยท derived/migration source"
   3722     );
   3723     assert!(sdk_root.join("event_store.sqlite").exists());
   3724     assert!(sdk_root.join("outbox.sqlite").exists());
   3725 
   3726     let legacy = sandbox.json_success(&["--format", "json", "store", "init"]);
   3727     assert_eq!(legacy["operation_id"], "store.init");
   3728 
   3729     let status_after_legacy = sandbox.json_success(&["--format", "json", "store", "status", "get"]);
   3730 
   3731     assert_eq!(
   3732         status_after_legacy["result"]["source"],
   3733         "SDK canonical event store and outbox"
   3734     );
   3735     assert_eq!(
   3736         status_after_legacy["result"]["legacy_replica"]["state"],
   3737         "ready"
   3738     );
   3739     assert_eq!(
   3740         status_after_legacy["result"]["legacy_replica"]["source"],
   3741         "legacy local replica ยท derived/migration source"
   3742     );
   3743 
   3744     let dry_run =
   3745         sandbox.json_success(&["--format", "json", "--dry-run", "store", "backup", "create"]);
   3746     let dry_run_destination = dry_run["result"]["destination"]
   3747         .as_str()
   3748         .expect("backup destination");
   3749     let dry_run_file = dry_run["result"]["file"].as_str().expect("backup file");
   3750 
   3751     assert_eq!(dry_run["operation_id"], "store.backup.create");
   3752     assert_eq!(dry_run["dry_run"], true);
   3753     assert_eq!(dry_run["result"]["state"], "dry_run");
   3754     assert_eq!(
   3755         dry_run["result"]["source"],
   3756         "SDK canonical event store and outbox"
   3757     );
   3758     assert_eq!(dry_run["result"]["backup_kind"], "sdk_canonical");
   3759     assert_eq!(dry_run["result"]["canonical_store"], "sdk");
   3760     assert_eq!(dry_run["result"]["size_bytes"], 0);
   3761     assert_eq!(
   3762         dry_run["result"]["manifest"]["manifest_kind"],
   3763         "sdk_canonical_backup_preview"
   3764     );
   3765     assert_eq!(
   3766         dry_run["result"]["manifest"]["backup_verification"]["event_store_ok"],
   3767         true
   3768     );
   3769     assert_eq!(
   3770         dry_run["result"]["manifest"]["backup_verification"]["outbox_ok"],
   3771         true
   3772     );
   3773     assert!(!Path::new(dry_run_destination).exists());
   3774     assert!(!Path::new(dry_run_file).exists());
   3775 
   3776     let backup = sandbox.json_success(&["--format", "json", "store", "backup", "create"]);
   3777     let backup_destination = backup["result"]["destination"]
   3778         .as_str()
   3779         .expect("backup destination");
   3780     let event_store_file = backup["result"]["event_store_file"]
   3781         .as_str()
   3782         .expect("event store backup");
   3783     let outbox_file = backup["result"]["outbox_file"]
   3784         .as_str()
   3785         .expect("outbox backup");
   3786     let manifest_file = backup["result"]["manifest_file"]
   3787         .as_str()
   3788         .expect("manifest backup");
   3789 
   3790     assert_eq!(backup["operation_id"], "store.backup.create");
   3791     assert_eq!(backup["result"]["state"], "completed");
   3792     assert_eq!(
   3793         backup["result"]["source"],
   3794         "SDK canonical event store and outbox"
   3795     );
   3796     assert_eq!(backup["result"]["backup_kind"], "sdk_canonical");
   3797     assert_eq!(backup["result"]["canonical_store"], "sdk");
   3798     assert_eq!(
   3799         backup["result"]["manifest"]["manifest_kind"],
   3800         "storage_backup"
   3801     );
   3802     assert!(
   3803         backup["result"]["size_bytes"]
   3804             .as_u64()
   3805             .expect("backup size")
   3806             > 0
   3807     );
   3808     assert!(Path::new(backup_destination).exists());
   3809     assert!(Path::new(event_store_file).exists());
   3810     assert!(Path::new(outbox_file).exists());
   3811     assert!(Path::new(manifest_file).exists());
   3812     assert_eq!(
   3813         backup["result"]["manifest"]["backup_verification"]["event_store_ok"],
   3814         true
   3815     );
   3816     assert_eq!(
   3817         backup["result"]["manifest"]["backup_verification"]["outbox_ok"],
   3818         true
   3819     );
   3820     assert!(
   3821         backup["result"]["manifest"]["source_paths"]["event_store_path"]
   3822             .as_str()
   3823             .expect("source event store path")
   3824             .contains("data/apps/cli/replica/sdk/event_store.sqlite")
   3825     );
   3826     assert!(!event_store_file.ends_with("replica.sqlite"));
   3827 
   3828     let restore_destination = sandbox.root().join("restored-sdk-store");
   3829     let restore_destination_arg = restore_destination.to_string_lossy().to_string();
   3830     let restore_dry_run = sandbox.json_success(&[
   3831         "--format",
   3832         "json",
   3833         "--dry-run",
   3834         "store",
   3835         "backup",
   3836         "restore",
   3837         backup_destination,
   3838         "--destination",
   3839         restore_destination_arg.as_str(),
   3840     ]);
   3841 
   3842     assert_eq!(restore_dry_run["operation_id"], "store.backup.restore");
   3843     assert_eq!(restore_dry_run["dry_run"], true);
   3844     assert_eq!(restore_dry_run["result"]["state"], "dry_run");
   3845     assert_eq!(
   3846         restore_dry_run["result"]["source"],
   3847         "SDK canonical event store and outbox"
   3848     );
   3849     assert_eq!(restore_dry_run["result"]["restore_kind"], "sdk_canonical");
   3850     assert_eq!(restore_dry_run["result"]["canonical_store"], "sdk");
   3851     assert_eq!(
   3852         restore_dry_run["result"]["backup_source"],
   3853         backup_destination
   3854     );
   3855     assert_eq!(
   3856         restore_dry_run["result"]["destination"],
   3857         restore_destination_arg
   3858     );
   3859     assert_eq!(
   3860         restore_dry_run["result"]["manifest"]["manifest_kind"],
   3861         "storage_backup"
   3862     );
   3863     assert_eq!(
   3864         restore_dry_run["result"]["verification"]["event_store_ok"],
   3865         true
   3866     );
   3867     assert_eq!(restore_dry_run["result"]["verification"]["outbox_ok"], true);
   3868     assert!(!restore_destination.exists());
   3869 
   3870     let restored = sandbox.json_success(&[
   3871         "--format",
   3872         "json",
   3873         "store",
   3874         "backup",
   3875         "restore",
   3876         backup_destination,
   3877         "--destination",
   3878         restore_destination_arg.as_str(),
   3879     ]);
   3880 
   3881     assert_eq!(restored["operation_id"], "store.backup.restore");
   3882     assert_eq!(restored["result"]["state"], "completed");
   3883     assert_eq!(
   3884         restored["result"]["source"],
   3885         "SDK canonical event store and outbox"
   3886     );
   3887     assert_eq!(restored["result"]["restore_kind"], "sdk_canonical");
   3888     assert_eq!(restored["result"]["canonical_store"], "sdk");
   3889     assert!(restore_destination.join("event_store.sqlite").exists());
   3890     assert!(restore_destination.join("outbox.sqlite").exists());
   3891     assert_eq!(
   3892         restored["result"]["restored_event_store_file"],
   3893         restore_destination
   3894             .join("event_store.sqlite")
   3895             .to_string_lossy()
   3896             .to_string()
   3897     );
   3898     assert_eq!(
   3899         restored["result"]["restored_outbox_file"],
   3900         restore_destination
   3901             .join("outbox.sqlite")
   3902             .to_string_lossy()
   3903             .to_string()
   3904     );
   3905 
   3906     let unapproved_overwrite = sandbox.json_output(&[
   3907         "--format",
   3908         "json",
   3909         "store",
   3910         "backup",
   3911         "restore",
   3912         backup_destination,
   3913         "--destination",
   3914         restore_destination_arg.as_str(),
   3915         "--overwrite",
   3916     ]);
   3917     assert!(!unapproved_overwrite.0.status.success());
   3918     assert_eq!(unapproved_overwrite.0.status.code(), Some(6));
   3919     assert_eq!(
   3920         unapproved_overwrite.1["operation_id"],
   3921         "store.backup.restore"
   3922     );
   3923     assert_eq!(
   3924         unapproved_overwrite.1["errors"][0]["code"],
   3925         "approval_required"
   3926     );
   3927 
   3928     let approved_overwrite = sandbox.json_success(&[
   3929         "--format",
   3930         "json",
   3931         "--approval-token",
   3932         "restore-ok",
   3933         "store",
   3934         "backup",
   3935         "restore",
   3936         backup_destination,
   3937         "--destination",
   3938         restore_destination_arg.as_str(),
   3939         "--overwrite",
   3940     ]);
   3941     assert_eq!(approved_overwrite["operation_id"], "store.backup.restore");
   3942     assert_eq!(approved_overwrite["result"]["state"], "completed");
   3943     assert_eq!(approved_overwrite["result"]["overwrite"], true);
   3944 
   3945     let missing_backup = sandbox.root().join("missing-sdk-backup");
   3946     let missing_backup_arg = missing_backup.to_string_lossy().to_string();
   3947     let (missing_output, missing_value) = sandbox.json_output(&[
   3948         "--format",
   3949         "json",
   3950         "store",
   3951         "backup",
   3952         "restore",
   3953         missing_backup_arg.as_str(),
   3954         "--destination",
   3955         sandbox
   3956             .root()
   3957             .join("missing-restore-destination")
   3958             .to_string_lossy()
   3959             .as_ref(),
   3960     ]);
   3961     assert!(!missing_output.status.success());
   3962     assert_eq!(missing_value["operation_id"], "store.backup.restore");
   3963     assert_eq!(missing_value["errors"][0]["code"], "io");
   3964     assert_eq!(missing_value["errors"][0]["detail"]["class"], "storage");
   3965 }
   3966 
   3967 #[test]
   3968 fn core_account_store_dry_runs_preflight_without_mutating_local_state() {
   3969     let sandbox = RadrootsCliSandbox::new();
   3970 
   3971     let workspace = sandbox.json_success(&["--format", "json", "--dry-run", "workspace", "init"]);
   3972     let workspace_db = workspace["result"]["local"]["path"]
   3973         .as_str()
   3974         .expect("workspace db path");
   3975     assert_eq!(workspace["operation_id"], "workspace.init");
   3976     assert_eq!(workspace["dry_run"], true);
   3977     assert_eq!(workspace["result"]["state"], "dry_run");
   3978     assert_eq!(workspace["result"]["local"]["replica_db"], "missing");
   3979     assert!(!Path::new(workspace_db).exists());
   3980 
   3981     let store = sandbox.json_success(&["--format", "json", "--dry-run", "store", "init"]);
   3982     let store_db = store["result"]["path"].as_str().expect("store db path");
   3983     assert_eq!(store["operation_id"], "store.init");
   3984     assert_eq!(store["dry_run"], true);
   3985     assert_eq!(store["result"]["state"], "dry_run");
   3986     assert_eq!(store["result"]["replica_db"], "missing");
   3987     assert!(!Path::new(store_db).exists());
   3988 
   3989     let account_create =
   3990         sandbox.json_success(&["--format", "json", "--dry-run", "account", "create"]);
   3991     assert_eq!(account_create["operation_id"], "account.create");
   3992     assert_eq!(account_create["dry_run"], true);
   3993     assert_eq!(account_create["result"]["state"], "dry_run");
   3994     assert_eq!(account_create["result"]["secret_backend"]["state"], "ready");
   3995 
   3996     let account_list = sandbox.json_success(&["--format", "json", "account", "list"]);
   3997     assert_eq!(account_list["result"]["count"], 0);
   3998 
   3999     let created = sandbox.json_success(&["--format", "json", "account", "create"]);
   4000     let account_id = created["result"]["account"]["id"]
   4001         .as_str()
   4002         .expect("account id");
   4003     let clear = sandbox.json_success(&[
   4004         "--format",
   4005         "json",
   4006         "--dry-run",
   4007         "account",
   4008         "selection",
   4009         "clear",
   4010     ]);
   4011     assert_eq!(clear["operation_id"], "account.selection.clear");
   4012     assert_eq!(clear["result"]["state"], "dry_run");
   4013     assert_eq!(clear["result"]["cleared_account"]["id"], account_id);
   4014     assert_eq!(clear["result"]["remaining_account_count"], 1);
   4015 
   4016     let selection = sandbox.json_success(&["--format", "json", "account", "selection", "get"]);
   4017     assert_eq!(
   4018         selection["result"]["account_resolution"]["default_account"]["id"],
   4019         account_id
   4020     );
   4021 }
   4022 
   4023 #[test]
   4024 fn seller_dry_runs_preflight_without_mutating_farm_or_listing_files() {
   4025     let sandbox = RadrootsCliSandbox::new();
   4026     sandbox.json_success(&["--format", "json", "account", "create"]);
   4027 
   4028     let farm_dry_run = sandbox.json_success(&[
   4029         "--format",
   4030         "json",
   4031         "--dry-run",
   4032         "farm",
   4033         "create",
   4034         "--name",
   4035         "Green Farm",
   4036         "--location",
   4037         "farmstand",
   4038         "--country",
   4039         "US",
   4040         "--delivery-method",
   4041         "pickup",
   4042     ]);
   4043     let farm_path = farm_dry_run["result"]["config"]["path"]
   4044         .as_str()
   4045         .expect("farm path");
   4046     assert_eq!(farm_dry_run["operation_id"], "farm.create");
   4047     assert_eq!(farm_dry_run["result"]["state"], "dry_run");
   4048     assert!(!Path::new(farm_path).exists());
   4049 
   4050     let missing_update = sandbox.json_success(&[
   4051         "--format",
   4052         "json",
   4053         "--dry-run",
   4054         "farm",
   4055         "profile",
   4056         "update",
   4057         "--value",
   4058         "Dry Name",
   4059     ]);
   4060     assert_eq!(missing_update["operation_id"], "farm.profile.update");
   4061     assert_eq!(missing_update["result"]["state"], "unconfigured");
   4062     assert!(!Path::new(farm_path).exists());
   4063 
   4064     let farm = sandbox.json_success(&[
   4065         "--format",
   4066         "json",
   4067         "farm",
   4068         "create",
   4069         "--name",
   4070         "Green Farm",
   4071         "--location",
   4072         "farmstand",
   4073         "--country",
   4074         "US",
   4075         "--delivery-method",
   4076         "pickup",
   4077     ]);
   4078     let farm_path = farm["result"]["config"]["path"]
   4079         .as_str()
   4080         .expect("farm path");
   4081     let farm_before = fs::read_to_string(farm_path).expect("farm before");
   4082     let farm_update = sandbox.json_success(&[
   4083         "--format",
   4084         "json",
   4085         "--dry-run",
   4086         "farm",
   4087         "profile",
   4088         "update",
   4089         "--value",
   4090         "Dry Name",
   4091     ]);
   4092     assert_eq!(farm_update["operation_id"], "farm.profile.update");
   4093     assert_eq!(farm_update["result"]["state"], "dry_run");
   4094     assert_eq!(farm_update["result"]["config"]["name"], "Dry Name");
   4095     assert_eq!(
   4096         fs::read_to_string(farm_path).expect("farm after dry-run"),
   4097         farm_before
   4098     );
   4099 
   4100     let listing_path = sandbox.root().join("dry-listing.toml");
   4101     let listing_path_arg = listing_path.to_string_lossy();
   4102     let listing_dry_run = sandbox.json_success(&[
   4103         "--format",
   4104         "json",
   4105         "--dry-run",
   4106         "listing",
   4107         "create",
   4108         "--output",
   4109         listing_path_arg.as_ref(),
   4110         "--key",
   4111         "eggs",
   4112         "--title",
   4113         "Eggs",
   4114         "--category",
   4115         "eggs",
   4116         "--summary",
   4117         "Fresh eggs",
   4118         "--bin-id",
   4119         "bin-1",
   4120         "--quantity-amount",
   4121         "1",
   4122         "--quantity-unit",
   4123         "each",
   4124         "--price-amount",
   4125         "6",
   4126         "--price-currency",
   4127         "USD",
   4128         "--price-per-amount",
   4129         "1",
   4130         "--price-per-unit",
   4131         "each",
   4132         "--available",
   4133         "10",
   4134     ]);
   4135     assert_eq!(listing_dry_run["operation_id"], "listing.create");
   4136     assert_eq!(listing_dry_run["result"]["state"], "dry_run");
   4137     assert_eq!(listing_dry_run["result"]["file"], listing_path_arg.as_ref());
   4138     assert!(!listing_path.exists());
   4139 
   4140     fs::write(&listing_path, "existing").expect("existing listing path");
   4141     let (collision_output, collision) = sandbox.json_output(&[
   4142         "--format",
   4143         "json",
   4144         "--dry-run",
   4145         "listing",
   4146         "create",
   4147         "--output",
   4148         listing_path_arg.as_ref(),
   4149         "--key",
   4150         "eggs",
   4151     ]);
   4152     assert!(!collision_output.status.success());
   4153     assert_eq!(collision["operation_id"], "listing.create");
   4154     assert_eq!(collision["errors"][0]["code"], "validation_failed");
   4155 
   4156     let listing_file = create_listing_draft(&sandbox, "seller-dry-run");
   4157     make_listing_publishable(
   4158         &listing_file,
   4159         farm["result"]["config"]["farm_d_tag"]
   4160             .as_str()
   4161             .expect("farm d tag"),
   4162     );
   4163     let listing_before = fs::read_to_string(&listing_file).expect("listing before");
   4164     let listing_update = sandbox.json_success(&[
   4165         "--format",
   4166         "json",
   4167         "--dry-run",
   4168         "listing",
   4169         "update",
   4170         listing_file.to_string_lossy().as_ref(),
   4171     ]);
   4172     assert_eq!(listing_update["operation_id"], "listing.update");
   4173     assert_eq!(listing_update["result"]["state"], "dry_run");
   4174     assert_eq!(
   4175         fs::read_to_string(&listing_file).expect("listing after dry-run"),
   4176         listing_before
   4177     );
   4178 }
   4179 
   4180 #[test]
   4181 fn seller_dry_runs_do_not_write_shared_local_work_records() {
   4182     let sandbox = RadrootsCliSandbox::new();
   4183     sandbox.json_success(&["--format", "json", "account", "create"]);
   4184 
   4185     sandbox.json_success(&[
   4186         "--format",
   4187         "json",
   4188         "--dry-run",
   4189         "farm",
   4190         "create",
   4191         "--name",
   4192         "Dry Run Farm",
   4193         "--location",
   4194         "farmstand",
   4195         "--country",
   4196         "US",
   4197         "--delivery-method",
   4198         "pickup",
   4199     ]);
   4200     assert!(sandbox.local_event_records().is_empty());
   4201 
   4202     let listing_path = sandbox.root().join("dry-run-local-work.toml");
   4203     let listing_path_arg = listing_path.to_string_lossy();
   4204     sandbox.json_success(&[
   4205         "--format",
   4206         "json",
   4207         "--dry-run",
   4208         "listing",
   4209         "create",
   4210         "--output",
   4211         listing_path_arg.as_ref(),
   4212         "--key",
   4213         "dry-run-eggs",
   4214         "--title",
   4215         "Eggs",
   4216         "--category",
   4217         "eggs",
   4218         "--summary",
   4219         "Fresh eggs",
   4220         "--bin-id",
   4221         "bin-1",
   4222         "--quantity-amount",
   4223         "1",
   4224         "--quantity-unit",
   4225         "each",
   4226         "--price-amount",
   4227         "6",
   4228         "--price-currency",
   4229         "USD",
   4230         "--price-per-amount",
   4231         "1",
   4232         "--price-per-unit",
   4233         "each",
   4234         "--available",
   4235         "10",
   4236     ]);
   4237     assert!(sandbox.local_event_records().is_empty());
   4238 }
   4239 
   4240 #[test]
   4241 fn seller_local_writes_append_shared_local_work_records() {
   4242     let sandbox = RadrootsCliSandbox::new();
   4243     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4244     let account_id = account["result"]["account"]["id"]
   4245         .as_str()
   4246         .expect("account id");
   4247     let farm = sandbox.json_success(&[
   4248         "--format",
   4249         "json",
   4250         "farm",
   4251         "create",
   4252         "--name",
   4253         "Green Farm",
   4254         "--location",
   4255         "farmstand",
   4256         "--country",
   4257         "US",
   4258         "--delivery-method",
   4259         "pickup",
   4260     ]);
   4261     let farm_config = &farm["result"]["config"];
   4262     let farm_d_tag = farm_config["farm_d_tag"].as_str().expect("farm d tag");
   4263     let seller_pubkey = farm_config["seller_pubkey"]
   4264         .as_str()
   4265         .expect("seller pubkey");
   4266     let listing_file = create_listing_draft(&sandbox, "shared-local-eggs");
   4267 
   4268     let records = sandbox.local_event_records();
   4269     assert_eq!(records.len(), 2);
   4270 
   4271     let farm_record = records
   4272         .iter()
   4273         .find(|record| {
   4274             record
   4275                 .local_work_json
   4276                 .as_ref()
   4277                 .and_then(|payload| payload["record_kind"].as_str())
   4278                 == Some("farm_config_v1")
   4279         })
   4280         .expect("farm local work record");
   4281     assert_eq!(farm_record.family, LocalRecordFamily::LocalWork);
   4282     assert_eq!(farm_record.status, LocalRecordStatus::LocalSaved);
   4283     assert_eq!(farm_record.source_runtime, SourceRuntime::Cli);
   4284     assert_eq!(farm_record.outbox_status, PublishOutboxStatus::None);
   4285     assert_eq!(farm_record.owner_account_id.as_deref(), Some(account_id));
   4286     assert_eq!(farm_record.owner_pubkey.as_deref(), Some(seller_pubkey));
   4287     assert_eq!(farm_record.farm_id.as_deref(), Some(farm_d_tag));
   4288     assert_eq!(farm_record.listing_addr, None);
   4289     let farm_payload = farm_record
   4290         .local_work_json
   4291         .as_ref()
   4292         .expect("farm local work payload");
   4293     assert_eq!(farm_payload["scope"], "workspace");
   4294     assert_eq!(farm_payload["document"]["farm"]["d_tag"], farm_d_tag);
   4295 
   4296     let listing_record = records
   4297         .iter()
   4298         .find(|record| {
   4299             record
   4300                 .local_work_json
   4301                 .as_ref()
   4302                 .and_then(|payload| payload["record_kind"].as_str())
   4303                 == Some("listing_draft_v1")
   4304         })
   4305         .expect("listing local work record");
   4306     assert_eq!(listing_record.family, LocalRecordFamily::LocalWork);
   4307     assert_eq!(listing_record.status, LocalRecordStatus::LocalSaved);
   4308     assert_eq!(listing_record.source_runtime, SourceRuntime::Cli);
   4309     assert_eq!(listing_record.outbox_status, PublishOutboxStatus::None);
   4310     assert_eq!(listing_record.owner_account_id.as_deref(), Some(account_id));
   4311     assert_eq!(listing_record.owner_pubkey.as_deref(), Some(seller_pubkey));
   4312     assert_eq!(listing_record.farm_id.as_deref(), Some(farm_d_tag));
   4313     assert!(
   4314         listing_record
   4315             .listing_addr
   4316             .as_deref()
   4317             .expect("listing addr")
   4318             .starts_with(format!("30402:{seller_pubkey}:").as_str())
   4319     );
   4320     let listing_payload = listing_record
   4321         .local_work_json
   4322         .as_ref()
   4323         .expect("listing local work payload");
   4324     assert_eq!(listing_payload["path"], listing_file.display().to_string());
   4325     assert_eq!(
   4326         listing_payload["document"]["product"]["key"],
   4327         "shared-local-eggs"
   4328     );
   4329 
   4330     let farm_update = sandbox.json_success(&[
   4331         "--format",
   4332         "json",
   4333         "farm",
   4334         "profile",
   4335         "update",
   4336         "--value",
   4337         "Green Farm Updated",
   4338     ]);
   4339     assert_eq!(farm_update["operation_id"], "farm.profile.update");
   4340     assert_eq!(farm_update["result"]["state"], "updated");
   4341     let second = sandbox.json_success(&["--format", "json", "account", "create"]);
   4342     let second_account_id = second["result"]["account"]["id"]
   4343         .as_str()
   4344         .expect("second account id");
   4345     sandbox.json_success(&[
   4346         "--format",
   4347         "json",
   4348         "--approval-token",
   4349         "approve",
   4350         "listing",
   4351         "rebind",
   4352         listing_file.to_string_lossy().as_ref(),
   4353         second_account_id,
   4354         "--farm-d-tag",
   4355         farm_d_tag,
   4356     ]);
   4357 
   4358     let updated_records = sandbox.local_event_records();
   4359     assert_eq!(updated_records.len(), 4);
   4360     let latest_farm_payload = updated_records
   4361         .iter()
   4362         .filter(|record| {
   4363             record
   4364                 .local_work_json
   4365                 .as_ref()
   4366                 .and_then(|payload| payload["record_kind"].as_str())
   4367                 == Some("farm_config_v1")
   4368         })
   4369         .max_by_key(|record| record.seq)
   4370         .and_then(|record| record.local_work_json.as_ref())
   4371         .expect("latest farm payload");
   4372     assert_eq!(
   4373         latest_farm_payload["document"]["profile"]["name"],
   4374         "Green Farm Updated"
   4375     );
   4376     let latest_listing = updated_records
   4377         .iter()
   4378         .filter(|record| {
   4379             record
   4380                 .local_work_json
   4381                 .as_ref()
   4382                 .and_then(|payload| payload["record_kind"].as_str())
   4383                 == Some("listing_draft_v1")
   4384         })
   4385         .max_by_key(|record| record.seq)
   4386         .expect("latest listing record");
   4387     assert_eq!(
   4388         latest_listing.owner_account_id.as_deref(),
   4389         Some(second_account_id)
   4390     );
   4391     let latest_listing_payload = latest_listing
   4392         .local_work_json
   4393         .as_ref()
   4394         .expect("latest listing payload");
   4395     assert_eq!(
   4396         latest_listing_payload["document"]["seller_actor"]["account_id"],
   4397         second_account_id
   4398     );
   4399 }
   4400 
   4401 #[test]
   4402 fn listing_app_records_list_and_export_to_valid_cli_draft() {
   4403     let sandbox = RadrootsCliSandbox::new();
   4404     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4405     let account_id = account["result"]["account"]["id"]
   4406         .as_str()
   4407         .expect("account id");
   4408     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4409     let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4410         .as_str()
   4411         .expect("seller pubkey");
   4412     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4413     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4414     seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag);
   4415     let listing_record_id = seed_app_listing_record(
   4416         &sandbox,
   4417         account_id,
   4418         seller_pubkey,
   4419         farm_d_tag,
   4420         listing_d_tag,
   4421     );
   4422 
   4423     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4424     assert_eq!(list["operation_id"], "listing.app.list");
   4425     assert_eq!(list["result"]["state"], "ready");
   4426     assert_eq!(list["result"]["count"], 2);
   4427     let listing_row = list["result"]["records"]
   4428         .as_array()
   4429         .expect("records")
   4430         .iter()
   4431         .find(|record| record["record_id"] == listing_record_id)
   4432         .expect("listing row");
   4433     assert_eq!(listing_row["record_kind"], "listing_draft_v1");
   4434     assert_eq!(listing_row["source_runtime"], "app");
   4435     assert_eq!(listing_row["exportable"], true);
   4436     assert_eq!(listing_row["listing_id"], listing_d_tag);
   4437     assert_eq!(listing_row["title"], "App Eggs");
   4438 
   4439     let export_path = sandbox.root().join("app-eggs.toml");
   4440     let export_path_arg = export_path.to_string_lossy();
   4441     let dry_run = sandbox.json_success(&[
   4442         "--format",
   4443         "json",
   4444         "--dry-run",
   4445         "listing",
   4446         "app",
   4447         "export",
   4448         listing_record_id.as_str(),
   4449         "--output",
   4450         export_path_arg.as_ref(),
   4451     ]);
   4452     assert_eq!(dry_run["operation_id"], "listing.app.export");
   4453     assert_eq!(dry_run["result"]["state"], "dry_run");
   4454     assert_eq!(dry_run["result"]["valid"], true);
   4455     assert!(!export_path.exists());
   4456 
   4457     let export = sandbox.json_success(&[
   4458         "--format",
   4459         "json",
   4460         "listing",
   4461         "app",
   4462         "export",
   4463         listing_record_id.as_str(),
   4464         "--output",
   4465         export_path_arg.as_ref(),
   4466     ]);
   4467     assert_eq!(export["operation_id"], "listing.app.export");
   4468     assert_eq!(export["result"]["state"], "exported");
   4469     assert_eq!(export["result"]["listing_id"], listing_d_tag);
   4470     assert_eq!(export["result"]["seller_account_id"], account_id);
   4471     assert!(export_path.exists());
   4472     let exported_contents = fs::read_to_string(&export_path).expect("exported listing draft");
   4473     assert!(exported_contents.contains("quantity_unit = \"each\""));
   4474     assert!(exported_contents.contains("price_per_unit = \"each\""));
   4475     assert!(exported_contents.contains("label = \"dozen\""));
   4476 
   4477     let validate = sandbox.json_success(&[
   4478         "--format",
   4479         "json",
   4480         "listing",
   4481         "validate",
   4482         export_path_arg.as_ref(),
   4483     ]);
   4484     assert_eq!(validate["operation_id"], "listing.validate");
   4485     assert_eq!(validate["result"]["valid"], true);
   4486     assert_eq!(validate["result"]["listing_id"], listing_d_tag);
   4487     assert_eq!(validate["result"]["seller_account_id"], account_id);
   4488 }
   4489 
   4490 #[test]
   4491 fn listing_app_records_list_current_records_and_blocks_stale_export() {
   4492     let sandbox = RadrootsCliSandbox::new();
   4493     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4494     let account_id = account["result"]["account"]["id"]
   4495         .as_str()
   4496         .expect("account id");
   4497     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4498     let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4499         .as_str()
   4500         .expect("seller pubkey");
   4501     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4502     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4503     seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag);
   4504     let stale_record_id = seed_app_listing_record_variant(
   4505         &sandbox,
   4506         account_id,
   4507         Some(seller_pubkey),
   4508         farm_d_tag,
   4509         listing_d_tag,
   4510         "stale",
   4511         "Old App Eggs",
   4512         None,
   4513     );
   4514     let current_record_id = seed_app_listing_record_variant(
   4515         &sandbox,
   4516         account_id,
   4517         Some(seller_pubkey),
   4518         farm_d_tag,
   4519         listing_d_tag,
   4520         "current",
   4521         "Current App Eggs",
   4522         None,
   4523     );
   4524 
   4525     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4526     assert_eq!(list["result"]["count"], 2);
   4527     assert_eq!(list["result"]["limit"], 500);
   4528     assert_eq!(list["result"]["has_more"], false);
   4529     let records = list["result"]["records"].as_array().expect("records");
   4530     assert!(
   4531         records
   4532             .iter()
   4533             .all(|record| record["record_id"] != stale_record_id)
   4534     );
   4535     let listing_row = records
   4536         .iter()
   4537         .find(|record| record["record_id"] == current_record_id)
   4538         .expect("current listing row");
   4539     assert_eq!(listing_row["title"], "Current App Eggs");
   4540     assert_eq!(listing_row["superseded_count"], 1);
   4541     assert_eq!(listing_row["exportable"], true);
   4542     assert!(
   4543         listing_row["change_seq"]
   4544             .as_i64()
   4545             .expect("listing change seq")
   4546             > records[1]["change_seq"].as_i64().expect("farm change seq")
   4547     );
   4548 
   4549     let export_path = sandbox.root().join("stale-app-eggs.toml");
   4550     let export_path_arg = export_path.to_string_lossy();
   4551     let (output, stale_export) = sandbox.json_output(&[
   4552         "--format",
   4553         "json",
   4554         "listing",
   4555         "app",
   4556         "export",
   4557         stale_record_id.as_str(),
   4558         "--output",
   4559         export_path_arg.as_ref(),
   4560     ]);
   4561     assert!(!output.status.success());
   4562     assert_eq!(stale_export["result"], Value::Null);
   4563     assert_eq!(stale_export["errors"][0]["detail"]["state"], "stale");
   4564     assert_eq!(stale_export["errors"][0]["detail"]["valid"], false);
   4565     assert!(
   4566         stale_export["errors"][0]["message"]
   4567             .as_str()
   4568             .expect("stale reason")
   4569             .contains(current_record_id.as_str())
   4570     );
   4571     assert!(!export_path.exists());
   4572 }
   4573 
   4574 #[test]
   4575 fn listing_app_records_list_includes_new_records_after_older_volume() {
   4576     let sandbox = RadrootsCliSandbox::new();
   4577     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4578     let account_id = account["result"]["account"]["id"]
   4579         .as_str()
   4580         .expect("account id");
   4581     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4582     let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4583         .as_str()
   4584         .expect("seller pubkey");
   4585     for index in 0..505 {
   4586         let farm_d_tag = format!("F{index:021}");
   4587         seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag.as_str());
   4588     }
   4589     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4590     let current_record_id = seed_app_listing_record_variant(
   4591         &sandbox,
   4592         account_id,
   4593         Some(seller_pubkey),
   4594         "AAAAAAAAAAAAAAAAAAAAAw",
   4595         listing_d_tag,
   4596         "current",
   4597         "Newest App Eggs",
   4598         None,
   4599     );
   4600 
   4601     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4602     assert_eq!(list["result"]["limit"], 500);
   4603     assert_eq!(list["result"]["count"], 500);
   4604     assert_eq!(list["result"]["has_more"], true);
   4605     assert!(list["result"]["next_before_change_seq"].as_i64().is_some());
   4606     assert!(list["result"]["next_before_seq"].as_i64().is_some());
   4607     let records = list["result"]["records"].as_array().expect("records");
   4608     assert_eq!(records[0]["record_id"], current_record_id);
   4609     assert!(
   4610         records
   4611             .iter()
   4612             .any(|record| record["record_id"] == current_record_id)
   4613     );
   4614 }
   4615 
   4616 #[test]
   4617 fn listing_app_records_keep_same_listing_id_separate_by_owner_pubkey() {
   4618     let sandbox = RadrootsCliSandbox::new();
   4619     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4620     let account_id = account["result"]["account"]["id"]
   4621         .as_str()
   4622         .expect("account id");
   4623     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4624     let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4625         .as_str()
   4626         .expect("seller pubkey");
   4627     let other_pubkey = identity_public(83).public_key_hex;
   4628     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4629     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4630     let first_record_id = seed_app_listing_record_variant_without_listing_addr(
   4631         &sandbox,
   4632         account_id,
   4633         Some(seller_pubkey),
   4634         farm_d_tag,
   4635         listing_d_tag,
   4636         "owner-one",
   4637         "First Owner Eggs",
   4638     );
   4639     let second_record_id = seed_app_listing_record_variant_without_listing_addr(
   4640         &sandbox,
   4641         "acct_owner_two",
   4642         Some(other_pubkey.as_str()),
   4643         farm_d_tag,
   4644         listing_d_tag,
   4645         "owner-two",
   4646         "Second Owner Eggs",
   4647     );
   4648 
   4649     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4650     assert_eq!(list["result"]["count"], 2);
   4651     let records = list["result"]["records"].as_array().expect("records");
   4652     let first_row = records
   4653         .iter()
   4654         .find(|record| record["record_id"] == first_record_id)
   4655         .expect("first owner row");
   4656     let second_row = records
   4657         .iter()
   4658         .find(|record| record["record_id"] == second_record_id)
   4659         .expect("second owner row");
   4660     assert_eq!(first_row["title"], "First Owner Eggs");
   4661     assert_eq!(second_row["title"], "Second Owner Eggs");
   4662     assert_eq!(first_row["superseded_count"], 0);
   4663     assert_eq!(second_row["superseded_count"], 0);
   4664     assert_eq!(first_row["exportable"], true);
   4665     assert_eq!(second_row["exportable"], true);
   4666 }
   4667 
   4668 #[test]
   4669 fn listing_app_records_export_blocks_stale_when_current_is_beyond_first_page() {
   4670     let sandbox = RadrootsCliSandbox::new();
   4671     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4672     let account_id = account["result"]["account"]["id"]
   4673         .as_str()
   4674         .expect("account id");
   4675     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4676     let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4677         .as_str()
   4678         .expect("seller pubkey");
   4679     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4680     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4681     let stale_record_id = seed_app_listing_record_variant(
   4682         &sandbox,
   4683         account_id,
   4684         Some(seller_pubkey),
   4685         farm_d_tag,
   4686         listing_d_tag,
   4687         "paged-stale",
   4688         "Paged Old Eggs",
   4689         None,
   4690     );
   4691     let current_record_id = seed_app_listing_record_variant(
   4692         &sandbox,
   4693         account_id,
   4694         Some(seller_pubkey),
   4695         farm_d_tag,
   4696         listing_d_tag,
   4697         "paged-current",
   4698         "Paged Current Eggs",
   4699         None,
   4700     );
   4701     for index in 0..505 {
   4702         let farm_d_tag = format!("G{index:021}");
   4703         seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag.as_str());
   4704     }
   4705 
   4706     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4707     assert_eq!(list["result"]["count"], 500);
   4708     assert_eq!(list["result"]["has_more"], true);
   4709     let records = list["result"]["records"].as_array().expect("records");
   4710     assert!(
   4711         records
   4712             .iter()
   4713             .all(|record| record["record_id"] != current_record_id)
   4714     );
   4715 
   4716     let export_path = sandbox.root().join("paged-stale-app-eggs.toml");
   4717     let export_path_arg = export_path.to_string_lossy();
   4718     let (output, stale_export) = sandbox.json_output(&[
   4719         "--format",
   4720         "json",
   4721         "listing",
   4722         "app",
   4723         "export",
   4724         stale_record_id.as_str(),
   4725         "--output",
   4726         export_path_arg.as_ref(),
   4727     ]);
   4728     assert!(!output.status.success());
   4729     assert_eq!(stale_export["result"], Value::Null);
   4730     assert_eq!(stale_export["errors"][0]["detail"]["state"], "stale");
   4731     assert!(
   4732         stale_export["errors"][0]["message"]
   4733             .as_str()
   4734             .expect("stale reason")
   4735             .contains(current_record_id.as_str())
   4736     );
   4737     assert!(!export_path.exists());
   4738 }
   4739 
   4740 #[test]
   4741 fn listing_app_records_mark_unresolved_pubkey_records_non_exportable() {
   4742     let sandbox = RadrootsCliSandbox::new();
   4743     let account_id = "acct_unresolved";
   4744     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4745     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4746     let record_id = seed_app_listing_record_variant(
   4747         &sandbox,
   4748         account_id,
   4749         None,
   4750         farm_d_tag,
   4751         listing_d_tag,
   4752         "unresolved",
   4753         "Unresolved App Eggs",
   4754         Some(json!({
   4755             "state": "identity_unresolved",
   4756             "reason": "canonical_hex_pubkey_required"
   4757         })),
   4758     );
   4759 
   4760     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4761     assert_eq!(list["result"]["count"], 1);
   4762     let listing_row = &list["result"]["records"][0];
   4763     assert_eq!(listing_row["record_id"], record_id);
   4764     assert_eq!(listing_row["title"], "Unresolved App Eggs");
   4765     assert_eq!(listing_row["exportable"], false);
   4766     assert_eq!(
   4767         listing_row["reason"],
   4768         "canonical hex pubkey required before export"
   4769     );
   4770     assert!(listing_row.get("listing_addr").is_none());
   4771 
   4772     let export_path = sandbox.root().join("unresolved-app-eggs.toml");
   4773     let export_path_arg = export_path.to_string_lossy();
   4774     let (output, export) = sandbox.json_output(&[
   4775         "--format",
   4776         "json",
   4777         "listing",
   4778         "app",
   4779         "export",
   4780         record_id.as_str(),
   4781         "--output",
   4782         export_path_arg.as_ref(),
   4783     ]);
   4784     assert!(!output.status.success());
   4785     assert_eq!(export["result"], Value::Null);
   4786     assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
   4787     assert_eq!(
   4788         export["errors"][0]["message"],
   4789         "canonical hex pubkey required before export"
   4790     );
   4791     assert!(!export_path.exists());
   4792 }
   4793 
   4794 #[test]
   4795 fn listing_app_records_ignore_body_pubkey_without_owner_metadata() {
   4796     let sandbox = RadrootsCliSandbox::new();
   4797     let account_id = "acct_body_only";
   4798     let body_pubkey = identity_public(91).public_key_hex;
   4799     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4800     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4801     let record_id = seed_app_listing_record_identity_variant(
   4802         &sandbox,
   4803         account_id,
   4804         Some(body_pubkey.as_str()),
   4805         None,
   4806         farm_d_tag,
   4807         listing_d_tag,
   4808         "body-only",
   4809         "Body Only App Eggs",
   4810         Some(json!({ "state": "exportable" })),
   4811         false,
   4812     );
   4813 
   4814     let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
   4815     assert_eq!(list["result"]["count"], 1);
   4816     let listing_row = &list["result"]["records"][0];
   4817     assert_eq!(listing_row["record_id"], record_id);
   4818     assert_eq!(listing_row["title"], "Body Only App Eggs");
   4819     assert_eq!(listing_row["exportable"], false);
   4820     assert_eq!(
   4821         listing_row["reason"],
   4822         "canonical hex pubkey required before export"
   4823     );
   4824     assert!(
   4825         listing_row
   4826             .get("actions")
   4827             .and_then(Value::as_array)
   4828             .is_none_or(Vec::is_empty)
   4829     );
   4830 
   4831     let export_path = sandbox.root().join("body-only-app-eggs.toml");
   4832     let export_path_arg = export_path.to_string_lossy();
   4833     let (output, export) = sandbox.json_output(&[
   4834         "--format",
   4835         "json",
   4836         "listing",
   4837         "app",
   4838         "export",
   4839         record_id.as_str(),
   4840         "--output",
   4841         export_path_arg.as_ref(),
   4842     ]);
   4843     assert!(!output.status.success());
   4844     assert_eq!(export["result"], Value::Null);
   4845     assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
   4846     assert_eq!(
   4847         export["errors"][0]["message"],
   4848         "canonical hex pubkey required before export"
   4849     );
   4850     assert!(!export_path.exists());
   4851 }
   4852 
   4853 #[test]
   4854 fn listing_app_records_export_uses_record_owner_over_body_pubkey() {
   4855     let sandbox = RadrootsCliSandbox::new();
   4856     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4857     let account_id = account["result"]["account"]["id"]
   4858         .as_str()
   4859         .expect("account id");
   4860     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4861     let owner_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4862         .as_str()
   4863         .expect("seller pubkey");
   4864     let body_pubkey = identity_public(92).public_key_hex;
   4865     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
   4866     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4867     let record_id = seed_app_listing_record_identity_variant(
   4868         &sandbox,
   4869         account_id,
   4870         Some(body_pubkey.as_str()),
   4871         Some(owner_pubkey),
   4872         farm_d_tag,
   4873         listing_d_tag,
   4874         "owner-wins",
   4875         "Owner Wins App Eggs",
   4876         None,
   4877         true,
   4878     );
   4879 
   4880     let export_path = sandbox.root().join("owner-wins-app-eggs.toml");
   4881     let export_path_arg = export_path.to_string_lossy();
   4882     let export = sandbox.json_success(&[
   4883         "--format",
   4884         "json",
   4885         "listing",
   4886         "app",
   4887         "export",
   4888         record_id.as_str(),
   4889         "--output",
   4890         export_path_arg.as_ref(),
   4891     ]);
   4892     assert_eq!(export["operation_id"], "listing.app.export");
   4893     assert_eq!(export["result"]["state"], "exported");
   4894     assert_eq!(export["result"]["seller_pubkey"], owner_pubkey);
   4895     let exported_contents = fs::read_to_string(&export_path).expect("exported listing draft");
   4896     assert!(exported_contents.contains(format!("pubkey = \"{owner_pubkey}\"").as_str()));
   4897     assert!(!exported_contents.contains(body_pubkey.as_str()));
   4898 }
   4899 
   4900 #[test]
   4901 fn order_app_records_list_export_get_and_submit_supported_app_order() {
   4902     let sandbox = RadrootsCliSandbox::new();
   4903     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   4904     let account_id = account["result"]["account"]["id"]
   4905         .as_str()
   4906         .expect("account id");
   4907     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   4908     let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   4909         .as_str()
   4910         .expect("buyer pubkey");
   4911     let seller_pubkey = identity_public(73).public_key_hex;
   4912     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   4913     let listing_addr = format!("30402:{seller_pubkey}:{listing_d_tag}");
   4914     let listing_event_id = seed_orderable_listing(&sandbox, listing_addr.as_str());
   4915     let order_id = "018f47a8-7b2c-7000-8000-000000000011";
   4916     let record_id = seed_app_order_record(
   4917         &sandbox,
   4918         account_id,
   4919         buyer_pubkey,
   4920         seller_pubkey.as_str(),
   4921         order_id,
   4922         listing_addr.as_str(),
   4923         listing_event_id.as_str(),
   4924     );
   4925 
   4926     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   4927     assert_eq!(app_list["operation_id"], "order.app.list");
   4928     assert_eq!(app_list["result"]["state"], "ready");
   4929     assert_eq!(app_list["result"]["count"], 1);
   4930     assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
   4931     assert_eq!(app_list["result"]["records"][0]["order_id"], order_id);
   4932     assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], true);
   4933     assert_eq!(app_list["result"]["records"][0]["exportable"], true);
   4934     assert_no_removed_command_reference(&app_list, &["order", "app", "list"]);
   4935     assert_no_daemon_runtime_reference(&app_list, &["order", "app", "list"]);
   4936 
   4937     let orders = sandbox.json_success(&["--format", "json", "order", "list"]);
   4938     assert_eq!(orders["operation_id"], "order.list");
   4939     assert_eq!(orders["result"]["state"], "ready");
   4940     assert_eq!(orders["result"]["count"], 1);
   4941     assert_eq!(orders["result"]["orders"][0]["id"], order_id);
   4942     assert_eq!(orders["result"]["orders"][0]["ready_for_submit"], true);
   4943     assert_eq!(
   4944         orders["result"]["orders"][0]["listing_event_id"],
   4945         listing_event_id
   4946     );
   4947     assert_eq!(
   4948         orders["result"]["orders"][0]["buyer_account_id"],
   4949         account_id
   4950     );
   4951     assert_eq!(
   4952         orders["result"]["orders"][0]["file"],
   4953         format!("shared-local-events/{record_id}")
   4954     );
   4955     assert_no_removed_command_reference(&orders, &["order", "list"]);
   4956     assert_no_daemon_runtime_reference(&orders, &["order", "list"]);
   4957 
   4958     let get_by_record =
   4959         sandbox.json_success(&["--format", "json", "order", "get", record_id.as_str()]);
   4960     assert_eq!(get_by_record["operation_id"], "order.get");
   4961     assert_eq!(get_by_record["result"]["state"], "ready");
   4962     assert_eq!(get_by_record["result"]["order_id"], order_id);
   4963     assert_eq!(get_by_record["result"]["ready_for_submit"], true);
   4964 
   4965     let export_path = sandbox.root().join("app-order.toml");
   4966     let export_path_arg = export_path.to_string_lossy();
   4967     let dry_run = sandbox.json_success(&[
   4968         "--format",
   4969         "json",
   4970         "--dry-run",
   4971         "order",
   4972         "app",
   4973         "export",
   4974         record_id.as_str(),
   4975         "--output",
   4976         export_path_arg.as_ref(),
   4977     ]);
   4978     assert_eq!(dry_run["operation_id"], "order.app.export");
   4979     assert_eq!(dry_run["result"]["state"], "dry_run");
   4980     assert_eq!(dry_run["result"]["valid"], true);
   4981     assert!(!export_path.exists());
   4982 
   4983     let export = sandbox.json_success(&[
   4984         "--format",
   4985         "json",
   4986         "order",
   4987         "app",
   4988         "export",
   4989         record_id.as_str(),
   4990         "--output",
   4991         export_path_arg.as_ref(),
   4992     ]);
   4993     assert_eq!(export["operation_id"], "order.app.export");
   4994     assert_eq!(export["result"]["state"], "exported");
   4995     assert_eq!(export["result"]["order_id"], order_id);
   4996     assert!(export_path.exists());
   4997     let exported_contents = fs::read_to_string(&export_path).expect("exported order draft");
   4998     assert!(exported_contents.contains("kind = \"order_draft_v1\""));
   4999     assert!(exported_contents.contains(format!("order_id = \"{order_id}\"").as_str()));
   5000     assert!(exported_contents.contains("source = \"resolved_account\""));
   5001 
   5002     let submit = sandbox.json_success(&[
   5003         "--format",
   5004         "json",
   5005         "--dry-run",
   5006         "order",
   5007         "submit",
   5008         record_id.as_str(),
   5009     ]);
   5010     assert_eq!(submit["operation_id"], "order.submit");
   5011     assert_eq!(submit["result"]["state"], "dry_run");
   5012     assert_eq!(submit["result"]["source"], "SDK order submit ยท local key");
   5013     assert_eq!(submit["result"]["event_kind"], 3422);
   5014     assert_eq!(
   5015         submit["result"]["target_relays"][0],
   5016         ORDERABLE_LISTING_RELAY
   5017     );
   5018     assert_eq!(
   5019         submit["result"]["event_id"]
   5020             .as_str()
   5021             .expect("event id")
   5022             .len(),
   5023         64
   5024     );
   5025 }
   5026 
   5027 #[test]
   5028 fn order_app_records_treat_matching_signed_evidence_as_submitted() {
   5029     let sandbox = RadrootsCliSandbox::new();
   5030     let buyer = identity_secret(97);
   5031     let buyer_public_file =
   5032         write_public_identity_profile(&sandbox, "app-order-submitted-buyer", &buyer.to_public());
   5033     let imported = sandbox.json_success(&[
   5034         "--format",
   5035         "json",
   5036         "--approval-token",
   5037         "approve",
   5038         "account",
   5039         "import",
   5040         "--default",
   5041         buyer_public_file.to_string_lossy().as_ref(),
   5042     ]);
   5043     let account_id = imported["result"]["account"]["id"]
   5044         .as_str()
   5045         .expect("account id");
   5046     let buyer_secret_file = write_secret_identity_profile(&sandbox, "app-order-submitted", &buyer);
   5047     sandbox.json_success(&[
   5048         "--format",
   5049         "json",
   5050         "--approval-token",
   5051         "approve",
   5052         "account",
   5053         "attach-secret",
   5054         account_id,
   5055         buyer_secret_file.to_string_lossy().as_ref(),
   5056         "--default",
   5057     ]);
   5058 
   5059     let buyer_pubkey = buyer.public_key_hex();
   5060     let seller_pubkey = identity_public(77).public_key_hex;
   5061     let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
   5062     let listing_addr = format!("30402:{seller_pubkey}:{listing_d_tag}");
   5063     let listing_event_id = seed_orderable_listing(&sandbox, listing_addr.as_str());
   5064     let order_id = "018f47a8-7b2c-7000-8000-000000000016";
   5065     let record_id = seed_app_order_record(
   5066         &sandbox,
   5067         account_id,
   5068         buyer_pubkey.as_str(),
   5069         seller_pubkey.as_str(),
   5070         order_id,
   5071         listing_addr.as_str(),
   5072         listing_event_id.as_str(),
   5073     );
   5074     let signed_event = signed_app_order_request_event(
   5075         &buyer,
   5076         order_id,
   5077         listing_addr.as_str(),
   5078         listing_event_id.as_str(),
   5079         seller_pubkey.as_str(),
   5080         2,
   5081     );
   5082     let signed_event_id = signed_event.id.to_hex();
   5083     append_app_signed_order_request_record(
   5084         &sandbox,
   5085         account_id,
   5086         listing_addr.as_str(),
   5087         &signed_event,
   5088     );
   5089 
   5090     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   5091     let listed = &app_list["result"]["records"][0];
   5092     assert_eq!(listed["record_id"], record_id);
   5093     assert_eq!(listed["status"], "submitted");
   5094     assert_eq!(listed["ready_for_submit"], false);
   5095     assert_eq!(listed["exportable"], false);
   5096     assert_eq!(
   5097         listed["actions"].as_array().expect("actions"),
   5098         &vec![json!(format!("radroots order status get {order_id}"))]
   5099     );
   5100 
   5101     let orders = sandbox.json_success(&["--format", "json", "order", "list"]);
   5102     assert_eq!(orders["result"]["state"], "ready");
   5103     assert_eq!(orders["result"]["orders"][0]["state"], "submitted");
   5104     assert_eq!(orders["result"]["orders"][0]["ready_for_submit"], false);
   5105 
   5106     let get_by_record =
   5107         sandbox.json_success(&["--format", "json", "order", "get", record_id.as_str()]);
   5108     assert_eq!(get_by_record["result"]["state"], "submitted");
   5109     assert_eq!(get_by_record["result"]["ready_for_submit"], false);
   5110     assert_eq!(
   5111         get_by_record["result"]["issues"][0]["code"],
   5112         "app_order_already_submitted"
   5113     );
   5114     assert_eq!(
   5115         get_by_record["result"]["issues"][0]["event_ids"][0],
   5116         signed_event_id
   5117     );
   5118     assert_eq!(
   5119         get_by_record["result"]["actions"]
   5120             .as_array()
   5121             .expect("actions"),
   5122         &vec![json!(format!("radroots order status get {order_id}"))]
   5123     );
   5124 
   5125     let export_path = sandbox.root().join("submitted-app-order.toml");
   5126     let export_path_arg = export_path.to_string_lossy();
   5127     let (export_output, export) = sandbox.json_output(&[
   5128         "--format",
   5129         "json",
   5130         "order",
   5131         "app",
   5132         "export",
   5133         record_id.as_str(),
   5134         "--output",
   5135         export_path_arg.as_ref(),
   5136     ]);
   5137     assert!(!export_output.status.success());
   5138     assert_eq!(export["operation_id"], "order.app.export");
   5139     assert_eq!(export["errors"][0]["detail"]["state"], "already_submitted");
   5140     assert_eq!(export["errors"][0]["detail"]["valid"], false);
   5141     assert!(!export_path.exists());
   5142 
   5143     let submit = sandbox.json_success(&[
   5144         "--format",
   5145         "json",
   5146         "--dry-run",
   5147         "order",
   5148         "submit",
   5149         record_id.as_str(),
   5150     ]);
   5151     assert_eq!(submit["operation_id"], "order.submit");
   5152     assert_eq!(submit["result"]["state"], "submitted");
   5153     assert_eq!(submit["result"]["deduplicated"], true);
   5154     assert_eq!(submit["result"]["event_id"], signed_event_id);
   5155     assert!(
   5156         submit["result"]
   5157             .get("actions")
   5158             .and_then(Value::as_array)
   5159             .is_none_or(Vec::is_empty)
   5160     );
   5161 }
   5162 
   5163 #[test]
   5164 fn order_app_records_fail_closed_when_signed_evidence_conflicts() {
   5165     let sandbox = RadrootsCliSandbox::new();
   5166     let buyer = identity_secret(98);
   5167     let buyer_public_file =
   5168         write_public_identity_profile(&sandbox, "app-order-conflict-buyer", &buyer.to_public());
   5169     let imported = sandbox.json_success(&[
   5170         "--format",
   5171         "json",
   5172         "--approval-token",
   5173         "approve",
   5174         "account",
   5175         "import",
   5176         "--default",
   5177         buyer_public_file.to_string_lossy().as_ref(),
   5178     ]);
   5179     let account_id = imported["result"]["account"]["id"]
   5180         .as_str()
   5181         .expect("account id");
   5182     let buyer_secret_file = write_secret_identity_profile(&sandbox, "app-order-conflict", &buyer);
   5183     sandbox.json_success(&[
   5184         "--format",
   5185         "json",
   5186         "--approval-token",
   5187         "approve",
   5188         "account",
   5189         "attach-secret",
   5190         account_id,
   5191         buyer_secret_file.to_string_lossy().as_ref(),
   5192         "--default",
   5193     ]);
   5194 
   5195     let buyer_pubkey = buyer.public_key_hex();
   5196     let seller_pubkey = identity_public(78).public_key_hex;
   5197     let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
   5198     let listing_event_id = seed_orderable_listing(&sandbox, listing_addr.as_str());
   5199     let order_id = "018f47a8-7b2c-7000-8000-000000000017";
   5200     let record_id = seed_app_order_record(
   5201         &sandbox,
   5202         account_id,
   5203         buyer_pubkey.as_str(),
   5204         seller_pubkey.as_str(),
   5205         order_id,
   5206         listing_addr.as_str(),
   5207         listing_event_id.as_str(),
   5208     );
   5209     let signed_event = signed_app_order_request_event(
   5210         &buyer,
   5211         order_id,
   5212         listing_addr.as_str(),
   5213         listing_event_id.as_str(),
   5214         seller_pubkey.as_str(),
   5215         3,
   5216     );
   5217     append_app_signed_order_request_record(
   5218         &sandbox,
   5219         account_id,
   5220         listing_addr.as_str(),
   5221         &signed_event,
   5222     );
   5223 
   5224     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   5225     let listed = &app_list["result"]["records"][0];
   5226     assert_eq!(listed["record_id"], record_id);
   5227     assert_eq!(listed["status"], "conflict");
   5228     assert_eq!(listed["ready_for_submit"], false);
   5229     assert_eq!(listed["exportable"], false);
   5230     assert!(
   5231         listed["reason"]
   5232             .as_str()
   5233             .expect("conflict reason")
   5234             .contains("conflicts")
   5235     );
   5236     assert!(
   5237         !listed["actions"]
   5238             .as_array()
   5239             .expect("actions")
   5240             .iter()
   5241             .any(|action| action
   5242                 .as_str()
   5243                 .expect("action")
   5244                 .contains("order app export"))
   5245     );
   5246 
   5247     let export_path = sandbox.root().join("conflicting-signed-app-order.toml");
   5248     let export_path_arg = export_path.to_string_lossy();
   5249     let (export_output, export) = sandbox.json_output(&[
   5250         "--format",
   5251         "json",
   5252         "order",
   5253         "app",
   5254         "export",
   5255         record_id.as_str(),
   5256         "--output",
   5257         export_path_arg.as_ref(),
   5258     ]);
   5259     assert!(!export_output.status.success());
   5260     assert_eq!(export["operation_id"], "order.app.export");
   5261     assert_eq!(export["errors"][0]["detail"]["state"], "conflict");
   5262     assert_eq!(
   5263         export["errors"][0]["detail"]["issues"][0]["code"],
   5264         "app_order_signed_evidence_conflict"
   5265     );
   5266     assert!(!export_path.exists());
   5267 
   5268     let (submit_output, submit) = sandbox.json_output(&[
   5269         "--format",
   5270         "json",
   5271         "--dry-run",
   5272         "order",
   5273         "submit",
   5274         record_id.as_str(),
   5275     ]);
   5276     assert!(!submit_output.status.success());
   5277     assert_eq!(submit["operation_id"], "order.submit");
   5278     assert_eq!(submit["errors"][0]["detail"]["state"], "invalid");
   5279     assert_eq!(
   5280         submit["errors"][0]["detail"]["issues"][0]["code"],
   5281         "app_order_signed_evidence_conflict"
   5282     );
   5283 }
   5284 
   5285 #[test]
   5286 fn order_app_records_fail_closed_when_not_current_or_supported() {
   5287     let sandbox = RadrootsCliSandbox::new();
   5288     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   5289     let account_id = account["result"]["account"]["id"]
   5290         .as_str()
   5291         .expect("account id");
   5292     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   5293     let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   5294         .as_str()
   5295         .expect("buyer pubkey");
   5296     let seller_pubkey = identity_public(74).public_key_hex;
   5297     let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
   5298     let listing_event_id = "2".repeat(64);
   5299     let stale_order_id = "018f47a8-7b2c-7000-8000-000000000012";
   5300     let stale_record_id = seed_app_order_record_variant(
   5301         &sandbox,
   5302         account_id,
   5303         buyer_pubkey,
   5304         seller_pubkey.as_str(),
   5305         stale_order_id,
   5306         listing_addr.as_str(),
   5307         listing_event_id.as_str(),
   5308         false,
   5309         "supported",
   5310         Vec::new(),
   5311     );
   5312 
   5313     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   5314     assert_eq!(
   5315         app_list["result"]["records"][0]["record_id"],
   5316         stale_record_id
   5317     );
   5318     assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
   5319     assert_eq!(app_list["result"]["records"][0]["exportable"], false);
   5320     assert!(
   5321         app_list["result"]["records"][0]["reason"]
   5322             .as_str()
   5323             .expect("stale reason")
   5324             .contains("not marked current")
   5325     );
   5326 
   5327     let export_path = sandbox.root().join("stale-app-order.toml");
   5328     let export_path_arg = export_path.to_string_lossy();
   5329     let (output, stale_export) = sandbox.json_output(&[
   5330         "--format",
   5331         "json",
   5332         "order",
   5333         "app",
   5334         "export",
   5335         stale_record_id.as_str(),
   5336         "--output",
   5337         export_path_arg.as_ref(),
   5338     ]);
   5339     assert!(!output.status.success());
   5340     assert_eq!(stale_export["operation_id"], "order.app.export");
   5341     assert_eq!(stale_export["result"], Value::Null);
   5342     assert_eq!(stale_export["errors"][0]["detail"]["state"], "stale");
   5343     assert_eq!(stale_export["errors"][0]["detail"]["valid"], false);
   5344     assert!(!export_path.exists());
   5345 
   5346     let (submit_output, submit) = sandbox.json_output(&[
   5347         "--format",
   5348         "json",
   5349         "--dry-run",
   5350         "order",
   5351         "submit",
   5352         stale_record_id.as_str(),
   5353     ]);
   5354     assert!(!submit_output.status.success());
   5355     assert_eq!(submit_output.status.code(), Some(3));
   5356     assert_eq!(submit["operation_id"], "order.submit");
   5357     assert_eq!(submit["errors"][0]["code"], "operation_unavailable");
   5358     assert_eq!(
   5359         submit["errors"][0]["detail"]["issues"][0]["code"],
   5360         "app_order_stale"
   5361     );
   5362 }
   5363 
   5364 #[test]
   5365 fn order_app_records_fail_closed_when_unsupported() {
   5366     let sandbox = RadrootsCliSandbox::new();
   5367     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   5368     let account_id = account["result"]["account"]["id"]
   5369         .as_str()
   5370         .expect("account id");
   5371     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   5372     let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   5373         .as_str()
   5374         .expect("buyer pubkey");
   5375     let seller_pubkey = identity_public(75).public_key_hex;
   5376     let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
   5377     let listing_event_id = "3".repeat(64);
   5378     let order_id = "018f47a8-7b2c-7000-8000-000000000013";
   5379     let record_id = seed_app_order_record_variant(
   5380         &sandbox,
   5381         account_id,
   5382         buyer_pubkey,
   5383         seller_pubkey.as_str(),
   5384         order_id,
   5385         listing_addr.as_str(),
   5386         listing_event_id.as_str(),
   5387         true,
   5388         "unsupported",
   5389         vec!["seller_pubkey_required"],
   5390     );
   5391 
   5392     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   5393     assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
   5394     assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
   5395     assert_eq!(app_list["result"]["records"][0]["exportable"], false);
   5396     assert!(
   5397         app_list["result"]["records"][0]["reason"]
   5398             .as_str()
   5399             .expect("unsupported reason")
   5400             .contains("not marked supported")
   5401     );
   5402 
   5403     let export_path = sandbox.root().join("unsupported-app-order.toml");
   5404     let export_path_arg = export_path.to_string_lossy();
   5405     let (export_output, export) = sandbox.json_output(&[
   5406         "--format",
   5407         "json",
   5408         "order",
   5409         "app",
   5410         "export",
   5411         record_id.as_str(),
   5412         "--output",
   5413         export_path_arg.as_ref(),
   5414     ]);
   5415     assert!(!export_output.status.success());
   5416     assert_eq!(export["operation_id"], "order.app.export");
   5417     assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
   5418     assert_eq!(
   5419         export["errors"][0]["detail"]["issues"][0]["code"],
   5420         "app_order_unsupported"
   5421     );
   5422     assert!(!export_path.exists());
   5423 
   5424     let (submit_output, submit) =
   5425         sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   5426     assert!(!submit_output.status.success());
   5427     assert_eq!(
   5428         submit["errors"][0]["detail"]["issues"][0]["code"],
   5429         "app_order_unsupported"
   5430     );
   5431 }
   5432 
   5433 #[test]
   5434 fn order_app_records_fail_closed_when_supported_record_is_malformed() {
   5435     let sandbox = RadrootsCliSandbox::new();
   5436     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   5437     let account_id = account["result"]["account"]["id"]
   5438         .as_str()
   5439         .expect("account id");
   5440     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   5441     let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   5442         .as_str()
   5443         .expect("buyer pubkey");
   5444     let seller_pubkey = identity_public(75).public_key_hex;
   5445     let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
   5446     let listing_event_id = "3".repeat(64);
   5447     let order_id = "018f47a8-7b2c-7000-8000-000000000014";
   5448     let record_id = seed_app_order_record_variant(
   5449         &sandbox,
   5450         account_id,
   5451         buyer_pubkey,
   5452         seller_pubkey.as_str(),
   5453         order_id,
   5454         listing_addr.as_str(),
   5455         listing_event_id.as_str(),
   5456         true,
   5457         "supported",
   5458         vec!["unit_price_required"],
   5459     );
   5460 
   5461     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   5462     assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
   5463     assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
   5464     assert_eq!(app_list["result"]["records"][0]["exportable"], false);
   5465     assert!(
   5466         app_list["result"]["records"][0]["reason"]
   5467             .as_str()
   5468             .expect("malformed reason")
   5469             .contains("support_status.issues")
   5470     );
   5471 
   5472     let export_path = sandbox.root().join("malformed-app-order.toml");
   5473     let export_path_arg = export_path.to_string_lossy();
   5474     let (export_output, export) = sandbox.json_output(&[
   5475         "--format",
   5476         "json",
   5477         "order",
   5478         "app",
   5479         "export",
   5480         record_id.as_str(),
   5481         "--output",
   5482         export_path_arg.as_ref(),
   5483     ]);
   5484     assert!(!export_output.status.success());
   5485     assert_eq!(export["operation_id"], "order.app.export");
   5486     assert_eq!(export["errors"][0]["detail"]["state"], "invalid");
   5487     assert_eq!(
   5488         export["errors"][0]["detail"]["issues"][0]["code"],
   5489         "invalid_app_order_record"
   5490     );
   5491     assert!(!export_path.exists());
   5492 
   5493     let (submit_output, submit) =
   5494         sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   5495     assert!(!submit_output.status.success());
   5496     assert_eq!(submit["operation_id"], "order.submit");
   5497     assert_eq!(
   5498         submit["errors"][0]["detail"]["issues"][0]["code"],
   5499         "invalid_app_order_record"
   5500     );
   5501 }
   5502 
   5503 #[test]
   5504 fn order_app_records_fail_closed_when_order_id_conflicts() {
   5505     let sandbox = RadrootsCliSandbox::new();
   5506     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   5507     let account_id = account["result"]["account"]["id"]
   5508         .as_str()
   5509         .expect("account id");
   5510     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   5511     let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   5512         .as_str()
   5513         .expect("buyer pubkey");
   5514     let seller_pubkey = identity_public(76).public_key_hex;
   5515     let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
   5516     let listing_event_id = "4".repeat(64);
   5517     let order_id = "018f47a8-7b2c-7000-8000-000000000015";
   5518     let first_record_id = seed_app_order_record(
   5519         &sandbox,
   5520         account_id,
   5521         buyer_pubkey,
   5522         seller_pubkey.as_str(),
   5523         order_id,
   5524         listing_addr.as_str(),
   5525         listing_event_id.as_str(),
   5526     );
   5527     let conflicting_record_id = format!("app:local_work:order_request:{order_id}:conflict");
   5528     seed_app_order_record_variant_with_record_id(
   5529         &sandbox,
   5530         account_id,
   5531         buyer_pubkey,
   5532         seller_pubkey.as_str(),
   5533         order_id,
   5534         listing_addr.as_str(),
   5535         listing_event_id.as_str(),
   5536         conflicting_record_id.clone(),
   5537         true,
   5538         "supported",
   5539         Vec::new(),
   5540     );
   5541 
   5542     let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
   5543     assert_eq!(app_list["result"]["count"], 1);
   5544     assert_eq!(
   5545         app_list["result"]["records"][0]["record_id"],
   5546         conflicting_record_id
   5547     );
   5548     assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
   5549     assert_eq!(app_list["result"]["records"][0]["exportable"], false);
   5550     assert!(
   5551         app_list["result"]["records"][0]["reason"]
   5552             .as_str()
   5553             .expect("conflict reason")
   5554             .contains(first_record_id.as_str())
   5555     );
   5556 
   5557     let export_path = sandbox.root().join("conflicting-app-order.toml");
   5558     let export_path_arg = export_path.to_string_lossy();
   5559     let (export_output, export) = sandbox.json_output(&[
   5560         "--format",
   5561         "json",
   5562         "order",
   5563         "app",
   5564         "export",
   5565         conflicting_record_id.as_str(),
   5566         "--output",
   5567         export_path_arg.as_ref(),
   5568     ]);
   5569     assert!(!export_output.status.success());
   5570     assert_eq!(export["operation_id"], "order.app.export");
   5571     assert_eq!(export["errors"][0]["detail"]["state"], "conflict");
   5572     assert_eq!(
   5573         export["errors"][0]["detail"]["issues"][0]["code"],
   5574         "app_order_conflict"
   5575     );
   5576     assert!(!export_path.exists());
   5577 
   5578     let (submit_output, submit) =
   5579         sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   5580     assert!(!submit_output.status.success());
   5581     assert_eq!(
   5582         submit["errors"][0]["detail"]["issues"][0]["code"],
   5583         "app_order_conflict"
   5584     );
   5585 }
   5586 
   5587 #[test]
   5588 fn farm_publish_uses_sdk_outbox_without_legacy_signed_event_records() {
   5589     let sandbox = RadrootsCliSandbox::new();
   5590     sandbox.json_success(&["--format", "json", "account", "create"]);
   5591     sandbox.json_success(&[
   5592         "--format",
   5593         "json",
   5594         "farm",
   5595         "create",
   5596         "--name",
   5597         "Green Farm",
   5598         "--location",
   5599         "farmstand",
   5600         "--country",
   5601         "US",
   5602         "--delivery-method",
   5603         "pickup",
   5604     ]);
   5605     let relay_url = "ws://127.0.0.1:9";
   5606     let local_event_records_before_publish = sandbox.local_event_records().len();
   5607 
   5608     let (output, publish) = sandbox.json_output(&[
   5609         "--format",
   5610         "json",
   5611         "--relay",
   5612         relay_url,
   5613         "--approval-token",
   5614         "approve",
   5615         "farm",
   5616         "publish",
   5617     ]);
   5618 
   5619     assert!(!output.status.success());
   5620     assert_eq!(publish["operation_id"], "farm.publish");
   5621     assert_eq!(publish["result"], serde_json::Value::Null);
   5622     assert_eq!(publish["errors"][0]["code"], "network_unavailable");
   5623     let detail = &publish["errors"][0]["detail"];
   5624     assert_eq!(detail["source"], "SDK farm publish ยท configured signer");
   5625     assert_eq!(detail["state"], "unavailable");
   5626     assert_eq!(detail["profile"]["state"], "not_submitted");
   5627     assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
   5628     assert_eq!(detail["farm"]["state"], "unavailable");
   5629     assert_eq!(detail["farm"]["target_relays"][0], relay_url);
   5630     assert_eq!(detail["farm"]["failed_relays"][0]["relay"], relay_url);
   5631     assert_eq!(
   5632         detail["farm"]["event_id"]
   5633             .as_str()
   5634             .expect("sdk farm event id")
   5635             .len(),
   5636         64
   5637     );
   5638 
   5639     let records = sandbox.local_event_records();
   5640     assert_eq!(records.len(), local_event_records_before_publish);
   5641     let signed_records = records
   5642         .iter()
   5643         .filter(|record| record.family == LocalRecordFamily::SignedEvent)
   5644         .collect::<Vec<_>>();
   5645     assert!(signed_records.is_empty());
   5646 }
   5647 
   5648 #[test]
   5649 fn listing_publish_failure_uses_sdk_outbox_without_legacy_local_event_record() {
   5650     let sandbox = RadrootsCliSandbox::new();
   5651     sandbox.json_success(&["--format", "json", "account", "create"]);
   5652     let farm = sandbox.json_success(&[
   5653         "--format",
   5654         "json",
   5655         "farm",
   5656         "create",
   5657         "--name",
   5658         "Green Farm",
   5659         "--location",
   5660         "farmstand",
   5661         "--country",
   5662         "US",
   5663         "--delivery-method",
   5664         "pickup",
   5665     ]);
   5666     let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
   5667         .as_str()
   5668         .expect("farm d tag");
   5669     let listing_file = create_listing_draft(&sandbox, "failed-outbox-eggs");
   5670     make_listing_publishable(&listing_file, farm_d_tag);
   5671     let relay_url = "ws://127.0.0.1:9";
   5672     let local_event_records_before_publish = sandbox.local_event_records().len();
   5673 
   5674     let (output, publish) = sandbox.json_output(&[
   5675         "--format",
   5676         "json",
   5677         "--relay",
   5678         relay_url,
   5679         "--approval-token",
   5680         "approve",
   5681         "listing",
   5682         "publish",
   5683         listing_file.to_string_lossy().as_ref(),
   5684     ]);
   5685 
   5686     assert!(!output.status.success());
   5687     assert_eq!(publish["operation_id"], "listing.publish");
   5688     assert_eq!(publish["errors"][0]["code"], "network_unavailable");
   5689     assert_eq!(
   5690         publish["errors"][0]["detail"]["source"],
   5691         "SDK listing publish ยท configured signer"
   5692     );
   5693     assert_eq!(publish["errors"][0]["detail"]["state"], "unavailable");
   5694     assert_eq!(
   5695         publish["errors"][0]["detail"]["target_relays"][0],
   5696         relay_url
   5697     );
   5698     assert_eq!(
   5699         publish["errors"][0]["detail"]["failed_relays"][0]["relay"],
   5700         relay_url
   5701     );
   5702     assert_eq!(
   5703         publish["errors"][0]["detail"]["actions"][0],
   5704         "radroots sync push"
   5705     );
   5706     assert_eq!(
   5707         publish["errors"][0]["detail"]["event_id"]
   5708             .as_str()
   5709             .expect("sdk event id")
   5710             .len(),
   5711         64
   5712     );
   5713     assert_eq!(
   5714         sandbox.local_event_records().len(),
   5715         local_event_records_before_publish
   5716     );
   5717 }
   5718 
   5719 #[test]
   5720 fn sync_push_sdk_outbox_failure_reports_network_unavailable() {
   5721     let sandbox = RadrootsCliSandbox::new();
   5722     sandbox.json_success(&["--format", "json", "account", "create"]);
   5723     let farm = sandbox.json_success(&[
   5724         "--format",
   5725         "json",
   5726         "farm",
   5727         "create",
   5728         "--name",
   5729         "Sync SDK Farm",
   5730         "--location",
   5731         "farmstand",
   5732         "--country",
   5733         "US",
   5734         "--delivery-method",
   5735         "pickup",
   5736     ]);
   5737     let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
   5738         .as_str()
   5739         .expect("farm d tag");
   5740     let listing_file = create_listing_draft(&sandbox, "sync-sdk-push-eggs");
   5741     make_listing_publishable(&listing_file, farm_d_tag);
   5742     let relay = "ws://127.0.0.1:9";
   5743     let publish = sandbox.json_success(&[
   5744         "--format",
   5745         "json",
   5746         "--offline",
   5747         "--relay",
   5748         relay,
   5749         "--approval-token",
   5750         "approve",
   5751         "listing",
   5752         "publish",
   5753         listing_file.to_string_lossy().as_ref(),
   5754     ]);
   5755 
   5756     assert_eq!(publish["operation_id"], "listing.publish");
   5757     assert_eq!(publish["result"]["state"], "queued");
   5758     let status = sandbox.json_success(&["--format", "json", "sync", "status", "get"]);
   5759     assert_eq!(
   5760         status["result"]["source"],
   5761         "SDK canonical event store and outbox"
   5762     );
   5763     assert_eq!(status["result"]["replica_db"], "legacy_derived_not_checked");
   5764     assert_eq!(status["result"]["queue"]["pending_count"], 1);
   5765     assert_eq!(status["result"]["queue"]["ready_signed_count"], 1);
   5766 
   5767     let (output, value) = sandbox.json_output(&[
   5768         "--format",
   5769         "json",
   5770         "--relay",
   5771         relay,
   5772         "--approval-token",
   5773         "approve",
   5774         "sync",
   5775         "push",
   5776     ]);
   5777 
   5778     assert!(!output.status.success(), "{value}");
   5779     assert_eq!(value["operation_id"], "sync.push");
   5780     assert_eq!(value["result"], Value::Null);
   5781     assert_eq!(value["errors"][0]["code"], "network_unavailable", "{value}");
   5782     assert_eq!(value["errors"][0]["detail"]["state"], "unavailable");
   5783     assert_eq!(value["errors"][0]["detail"]["source"], "SDK outbox push");
   5784     assert_eq!(
   5785         value["errors"][0]["detail"]["replica_db"],
   5786         "legacy_derived_not_checked"
   5787     );
   5788     assert_eq!(value["errors"][0]["detail"]["target_relays"][0], relay);
   5789     assert_eq!(
   5790         value["errors"][0]["detail"]["failed_relays"][0]["relay"],
   5791         relay
   5792     );
   5793     assert_eq!(value["errors"][0]["detail"]["publishable_count"], 1);
   5794     assert_eq!(value["errors"][0]["detail"]["published_count"], 0);
   5795     assert_eq!(value["errors"][0]["detail"]["failed_count"], 1);
   5796     assert_eq!(
   5797         value["errors"][0]["detail"]["reason"],
   5798         "SDK outbox push did not reach accepted quorum for any ready event"
   5799     );
   5800     assert_eq!(
   5801         value["errors"][0]["detail"]["actions"][0],
   5802         "radroots sync push"
   5803     );
   5804     assert_no_removed_command_reference(&value, &["sync", "push"]);
   5805     assert_no_daemon_runtime_reference(&value, &["sync", "push"]);
   5806 }
   5807 
   5808 #[test]
   5809 fn sync_push_ignores_legacy_replica_pending_queue_for_sdk_canonical_push() {
   5810     let sandbox = RadrootsCliSandbox::new();
   5811     sandbox.json_success(&["--format", "json", "account", "create"]);
   5812     sandbox.json_success(&["--format", "json", "store", "init"]);
   5813     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   5814     let selected_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
   5815         .as_str()
   5816         .expect("selected public key");
   5817     seed_legacy_replica_sync_farm(&sandbox, LEGACY_SYNC_PUSH_FARM_D_TAG, selected_pubkey);
   5818     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica");
   5819     let legacy_batch =
   5820         radroots_replica_pending_publish_batch(&executor).expect("legacy pending batch");
   5821     assert!(legacy_batch.pending_count > 0);
   5822 
   5823     let value = sandbox.json_success(&[
   5824         "--format",
   5825         "json",
   5826         "--relay",
   5827         "ws://127.0.0.1:9",
   5828         "--approval-token",
   5829         "approve",
   5830         "sync",
   5831         "push",
   5832     ]);
   5833 
   5834     assert_eq!(value["operation_id"], "sync.push");
   5835     assert_eq!(value["result"]["state"], "ready");
   5836     assert_eq!(value["result"]["source"], "SDK outbox push");
   5837     assert_eq!(value["result"]["replica_db"], "legacy_derived_not_checked");
   5838     assert_eq!(value["result"]["queue"]["pending_count"], 0);
   5839     assert_eq!(value["result"]["queue"]["total_count"], 0);
   5840     assert_eq!(value["result"]["publishable_count"], 0);
   5841     assert_eq!(value["result"]["published_count"], 0);
   5842     assert_eq!(value["result"]["failed_count"], 0);
   5843     assert_eq!(
   5844         value["result"]["reason"],
   5845         "SDK outbox had no ready signed events to push"
   5846     );
   5847     assert_no_removed_command_reference(&value, &["sync", "push"]);
   5848     assert_no_daemon_runtime_reference(&value, &["sync", "push"]);
   5849 }
   5850 
   5851 #[test]
   5852 fn buyer_market_sync_basket_dry_runs_preflight_without_mutating_local_state() {
   5853     let sandbox = RadrootsCliSandbox::new();
   5854     sandbox.json_success(&["--format", "json", "account", "create"]);
   5855 
   5856     let market = sandbox.json_success(&["--format", "json", "--dry-run", "market", "refresh"]);
   5857     assert_eq!(market["operation_id"], "market.refresh");
   5858     assert_eq!(market["dry_run"], true);
   5859     assert_eq!(market["result"]["state"], "unconfigured");
   5860     assert_eq!(market["result"]["replica_db"], "missing");
   5861 
   5862     let (sync_pull_output, sync_pull) =
   5863         sandbox.json_output(&["--format", "json", "--dry-run", "sync", "pull"]);
   5864     assert!(!sync_pull_output.status.success());
   5865     assert_eq!(sync_pull["operation_id"], "sync.pull");
   5866     assert_eq!(sync_pull["dry_run"], true);
   5867     assert_eq!(sync_pull["errors"][0]["code"], "operation_unavailable");
   5868     assert_eq!(sync_pull["errors"][0]["detail"]["state"], "unconfigured");
   5869     assert_eq!(sync_pull["errors"][0]["detail"]["replica_db"], "missing");
   5870 
   5871     let sync_push = sandbox.json_success(&["--format", "json", "--dry-run", "sync", "push"]);
   5872     assert_eq!(sync_push["operation_id"], "sync.push");
   5873     assert_eq!(sync_push["dry_run"], true);
   5874     assert_eq!(sync_push["result"]["state"], "ready");
   5875     assert_eq!(sync_push["result"]["source"], "SDK outbox push");
   5876     assert_eq!(
   5877         sync_push["result"]["replica_db"],
   5878         "legacy_derived_not_checked"
   5879     );
   5880     assert_eq!(sync_push["result"]["queue"]["pending_count"], 0);
   5881     assert_eq!(sync_push["result"]["queue"]["total_count"], 0);
   5882     assert_eq!(sync_push["result"]["publishable_count"], 0);
   5883     assert_eq!(sync_push["result"]["published_count"], 0);
   5884 
   5885     sandbox.json_success(&["--format", "json", "store", "init"]);
   5886     let relay_refresh = sandbox.json_success(&[
   5887         "--format",
   5888         "json",
   5889         "--relay",
   5890         "ws://127.0.0.1:9",
   5891         "--dry-run",
   5892         "market",
   5893         "refresh",
   5894     ]);
   5895     assert_eq!(relay_refresh["operation_id"], "market.refresh");
   5896     assert_eq!(relay_refresh["dry_run"], true);
   5897     assert_eq!(relay_refresh["result"]["state"], "ready");
   5898     assert_eq!(
   5899         relay_refresh["result"]["target_relays"][0],
   5900         "ws://127.0.0.1:9"
   5901     );
   5902     assert_eq!(relay_refresh["result"]["fetched_count"], 0);
   5903     assert_eq!(relay_refresh["result"]["ingested_count"], 0);
   5904 
   5905     let sync_push_ready = sandbox.json_success(&[
   5906         "--format",
   5907         "json",
   5908         "--relay",
   5909         "ws://127.0.0.1:9",
   5910         "--dry-run",
   5911         "sync",
   5912         "push",
   5913     ]);
   5914     assert_eq!(sync_push_ready["operation_id"], "sync.push");
   5915     assert_eq!(sync_push_ready["dry_run"], true);
   5916     assert_eq!(sync_push_ready["result"]["state"], "ready");
   5917     assert_eq!(sync_push_ready["result"]["source"], "SDK outbox push");
   5918     assert_eq!(
   5919         sync_push_ready["result"]["replica_db"],
   5920         "legacy_derived_not_checked"
   5921     );
   5922     assert_eq!(
   5923         sync_push_ready["result"]["target_relays"][0],
   5924         "ws://127.0.0.1:9"
   5925     );
   5926     assert_eq!(sync_push_ready["result"]["publishable_count"], 0);
   5927     assert_eq!(sync_push_ready["result"]["published_count"], 0);
   5928 
   5929     let empty_search =
   5930         sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
   5931     assert_eq!(empty_search["operation_id"], "market.product.search");
   5932     assert_eq!(empty_search["result"]["state"], "empty");
   5933 
   5934     let create_dry_run = sandbox.json_success(&[
   5935         "--format",
   5936         "json",
   5937         "--dry-run",
   5938         "basket",
   5939         "create",
   5940         "basket_probe",
   5941     ]);
   5942     let basket_file = create_dry_run["result"]["file"]
   5943         .as_str()
   5944         .expect("basket file");
   5945     assert_eq!(create_dry_run["operation_id"], "basket.create");
   5946     assert_eq!(create_dry_run["result"]["state"], "dry_run");
   5947     assert!(!Path::new(basket_file).exists());
   5948 
   5949     sandbox.json_success(&["--format", "json", "basket", "create", "basket_probe"]);
   5950     let (collision_output, collision) = sandbox.json_output(&[
   5951         "--format",
   5952         "json",
   5953         "--dry-run",
   5954         "basket",
   5955         "create",
   5956         "basket_probe",
   5957     ]);
   5958     assert!(!collision_output.status.success());
   5959     assert_eq!(collision["operation_id"], "basket.create");
   5960     assert_eq!(collision["errors"][0]["code"], "invalid_input");
   5961 
   5962     let before_add = sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
   5963     let add = sandbox.json_success(&[
   5964         "--format",
   5965         "json",
   5966         "--dry-run",
   5967         "basket",
   5968         "item",
   5969         "add",
   5970         "basket_probe",
   5971         "--listing-addr",
   5972         LISTING_ADDR,
   5973         "--bin-id",
   5974         "bin-1",
   5975         "--quantity",
   5976         "2",
   5977     ]);
   5978     assert_eq!(add["operation_id"], "basket.item.add");
   5979     assert_eq!(add["result"]["state"], "dry_run");
   5980     let after_add = sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
   5981     assert_eq!(after_add["result"], before_add["result"]);
   5982 
   5983     sandbox.json_success(&[
   5984         "--format",
   5985         "json",
   5986         "basket",
   5987         "item",
   5988         "add",
   5989         "basket_probe",
   5990         "--listing-addr",
   5991         LISTING_ADDR,
   5992         "--bin-id",
   5993         "bin-1",
   5994         "--quantity",
   5995         "2",
   5996     ]);
   5997     let quote = sandbox.json_success(&[
   5998         "--format",
   5999         "json",
   6000         "--dry-run",
   6001         "basket",
   6002         "quote",
   6003         "create",
   6004         "basket_probe",
   6005     ]);
   6006     assert_eq!(quote["operation_id"], "basket.quote.create");
   6007     assert_eq!(quote["result"]["state"], "unconfigured");
   6008     assert_eq!(quote["result"]["ready_for_quote"], false);
   6009     assert_eq!(
   6010         quote["result"]["issues"][0]["code"],
   6011         "basket_item_listing_unresolved"
   6012     );
   6013 
   6014     let basket_after_quote =
   6015         sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
   6016     assert_eq!(basket_after_quote["result"]["quote"], Value::Null);
   6017 }
   6018 
   6019 #[test]
   6020 fn market_order_request_readiness_gates_buyer_intent_actions() {
   6021     let sandbox = RadrootsCliSandbox::new();
   6022     seed_orderable_listing(&sandbox, LISTING_ADDR);
   6023 
   6024     let search = sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
   6025     assert_eq!(search["operation_id"], "market.product.search");
   6026     let result = &search["result"]["results"][0];
   6027     assert_eq!(result["protocol_valid"], true);
   6028     assert_eq!(result["marketplace_eligible"], true);
   6029     assert_eq!(result["order_request_enabled"], true);
   6030     assert_eq!(result["primary_bin_verified"], true);
   6031     assert!(result.get("reason_codes").is_none());
   6032     assert!(
   6033         search["result"]["actions"]
   6034             .as_array()
   6035             .expect("search actions")
   6036             .iter()
   6037             .any(|action| action == "radroots basket create")
   6038     );
   6039 
   6040     let listing = sandbox.json_success(&[
   6041         "--format",
   6042         "json",
   6043         "market",
   6044         "listing",
   6045         "get",
   6046         "pasture-eggs",
   6047     ]);
   6048     assert_eq!(listing["operation_id"], "market.listing.get");
   6049     assert_eq!(listing["result"]["protocol_valid"], true);
   6050     assert_eq!(listing["result"]["marketplace_eligible"], true);
   6051     assert_eq!(listing["result"]["order_request_enabled"], true);
   6052     assert_eq!(listing["result"]["primary_bin_verified"], true);
   6053     assert!(
   6054         listing["result"]["actions"]
   6055             .as_array()
   6056             .expect("listing actions")
   6057             .iter()
   6058             .any(|action| action == "radroots basket create")
   6059     );
   6060 
   6061     update_orderable_listing_available_amount(&sandbox, LISTING_ADDR, 0);
   6062 
   6063     let disabled_search =
   6064         sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
   6065     let disabled_result = &disabled_search["result"]["results"][0];
   6066     assert_eq!(disabled_result["protocol_valid"], true);
   6067     assert_eq!(disabled_result["marketplace_eligible"], true);
   6068     assert_eq!(disabled_result["order_request_enabled"], false);
   6069     assert_eq!(disabled_result["primary_bin_verified"], true);
   6070     assert_eq!(
   6071         disabled_result["reason_codes"][0],
   6072         "listing_order_request_disabled"
   6073     );
   6074     assert_eq!(
   6075         disabled_result["reason_codes"][1],
   6076         "listing_inventory_unavailable"
   6077     );
   6078     assert!(
   6079         disabled_search["result"]["actions"]
   6080             .as_array()
   6081             .expect("disabled search actions")
   6082             .iter()
   6083             .all(|action| action != "radroots basket create")
   6084     );
   6085 
   6086     let disabled_listing = sandbox.json_success(&[
   6087         "--format",
   6088         "json",
   6089         "market",
   6090         "listing",
   6091         "get",
   6092         "pasture-eggs",
   6093     ]);
   6094     assert_eq!(disabled_listing["result"]["protocol_valid"], true);
   6095     assert_eq!(disabled_listing["result"]["marketplace_eligible"], true);
   6096     assert_eq!(disabled_listing["result"]["order_request_enabled"], false);
   6097     assert_eq!(disabled_listing["result"]["primary_bin_verified"], true);
   6098     assert_eq!(
   6099         disabled_listing["result"]["reason_codes"][0],
   6100         "listing_order_request_disabled"
   6101     );
   6102     assert!(
   6103         disabled_listing["result"]
   6104             .get("actions")
   6105             .and_then(Value::as_array)
   6106             .is_none_or(Vec::is_empty)
   6107     );
   6108 
   6109     update_orderable_listing_available_amount(&sandbox, LISTING_ADDR, 5);
   6110     update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, None);
   6111 
   6112     let no_bin_search =
   6113         sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
   6114     let no_bin_result = &no_bin_search["result"]["results"][0];
   6115     assert_eq!(no_bin_result["primary_bin_id"], Value::Null);
   6116     assert_eq!(no_bin_result["order_request_enabled"], false);
   6117     assert_eq!(no_bin_result["primary_bin_verified"], false);
   6118     assert_eq!(
   6119         no_bin_result["reason_codes"][0],
   6120         "listing_order_request_disabled"
   6121     );
   6122     assert_eq!(
   6123         no_bin_result["reason_codes"][1],
   6124         "listing_primary_bin_missing"
   6125     );
   6126     assert!(
   6127         no_bin_search["result"]["actions"]
   6128             .as_array()
   6129             .expect("no-bin search actions")
   6130             .iter()
   6131             .all(|action| action != "radroots basket create")
   6132     );
   6133 
   6134     let no_bin_listing = sandbox.json_success(&[
   6135         "--format",
   6136         "json",
   6137         "market",
   6138         "listing",
   6139         "get",
   6140         "pasture-eggs",
   6141     ]);
   6142     assert_eq!(no_bin_listing["result"]["primary_bin_id"], Value::Null);
   6143     assert_eq!(no_bin_listing["result"]["order_request_enabled"], false);
   6144     assert_eq!(no_bin_listing["result"]["primary_bin_verified"], false);
   6145     assert_eq!(
   6146         no_bin_listing["result"]["reason_codes"][1],
   6147         "listing_primary_bin_missing"
   6148     );
   6149     assert!(
   6150         no_bin_listing["result"]
   6151             .get("actions")
   6152             .and_then(Value::as_array)
   6153             .is_none_or(Vec::is_empty)
   6154     );
   6155 
   6156     update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("missing-bin"));
   6157 
   6158     let invalid_bin_search =
   6159         sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
   6160     let invalid_bin_result = &invalid_bin_search["result"]["results"][0];
   6161     assert_eq!(invalid_bin_result["primary_bin_id"], "missing-bin");
   6162     assert_eq!(invalid_bin_result["order_request_enabled"], false);
   6163     assert_eq!(invalid_bin_result["primary_bin_verified"], false);
   6164     assert_eq!(
   6165         invalid_bin_result["reason_codes"][1],
   6166         "listing_primary_bin_invalid"
   6167     );
   6168     assert!(
   6169         invalid_bin_search["result"]["actions"]
   6170             .as_array()
   6171             .expect("invalid-bin search actions")
   6172             .iter()
   6173             .all(|action| action != "radroots basket create")
   6174     );
   6175 
   6176     let invalid_bin_listing = sandbox.json_success(&[
   6177         "--format",
   6178         "json",
   6179         "market",
   6180         "listing",
   6181         "get",
   6182         "pasture-eggs",
   6183     ]);
   6184     assert_eq!(
   6185         invalid_bin_listing["result"]["primary_bin_id"],
   6186         "missing-bin"
   6187     );
   6188     assert_eq!(
   6189         invalid_bin_listing["result"]["order_request_enabled"],
   6190         false
   6191     );
   6192     assert_eq!(invalid_bin_listing["result"]["primary_bin_verified"], false);
   6193     assert_eq!(
   6194         invalid_bin_listing["result"]["reason_codes"][1],
   6195         "listing_primary_bin_invalid"
   6196     );
   6197     assert!(
   6198         invalid_bin_listing["result"]
   6199             .get("actions")
   6200             .and_then(Value::as_array)
   6201             .is_none_or(Vec::is_empty)
   6202     );
   6203 
   6204     update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("bin-1"));
   6205 
   6206     let restored_listing = sandbox.json_success(&[
   6207         "--format",
   6208         "json",
   6209         "market",
   6210         "listing",
   6211         "get",
   6212         "pasture-eggs",
   6213     ]);
   6214     assert_eq!(restored_listing["result"]["primary_bin_id"], "bin-1");
   6215     assert_eq!(restored_listing["result"]["order_request_enabled"], true);
   6216     assert_eq!(restored_listing["result"]["primary_bin_verified"], true);
   6217     assert!(
   6218         restored_listing["result"]
   6219             .get("reason_codes")
   6220             .is_none_or(Value::is_null)
   6221     );
   6222     assert!(
   6223         restored_listing["result"]["actions"]
   6224             .as_array()
   6225             .expect("restored listing actions")
   6226             .iter()
   6227             .any(|action| action == "radroots basket create")
   6228     );
   6229 }
   6230 
   6231 #[test]
   6232 fn required_approval_token_rejects_absent_empty_and_whitespace_values() {
   6233     let sandbox = RadrootsCliSandbox::new();
   6234     let public_identity = identity_public(61);
   6235     let public_identity_file =
   6236         write_public_identity_profile(&sandbox, "approval-import", &public_identity);
   6237     let public_identity_path = public_identity_file.to_string_lossy();
   6238 
   6239     assert_required_approval_token_rejected(
   6240         &sandbox,
   6241         "account.import",
   6242         &["account", "import", public_identity_path.as_ref()],
   6243     );
   6244     assert_required_approval_token_rejected(
   6245         &sandbox,
   6246         "account.remove",
   6247         &["account", "remove", "acct_missing"],
   6248     );
   6249     assert_required_approval_token_rejected(
   6250         &sandbox,
   6251         "farm.rebind",
   6252         &["farm", "rebind", "acct_missing"],
   6253     );
   6254     assert_required_approval_token_rejected(&sandbox, "farm.publish", &["farm", "publish"]);
   6255     assert_required_approval_token_rejected(
   6256         &sandbox,
   6257         "listing.publish",
   6258         &["listing", "publish", "missing-listing.toml"],
   6259     );
   6260     assert_required_approval_token_rejected(
   6261         &sandbox,
   6262         "listing.update",
   6263         &["listing", "update", "missing-listing.toml"],
   6264     );
   6265     assert_required_approval_token_rejected(
   6266         &sandbox,
   6267         "listing.archive",
   6268         &["listing", "archive", "missing-listing.toml"],
   6269     );
   6270     assert_required_approval_token_rejected(
   6271         &sandbox,
   6272         "sync.push",
   6273         &["--relay", "ws://127.0.0.1:9", "sync", "push"],
   6274     );
   6275     assert_required_approval_token_rejected(&sandbox, "order.submit", &["order", "submit"]);
   6276     assert_required_approval_token_rejected(
   6277         &sandbox,
   6278         "order.rebind",
   6279         &["order", "rebind", "ord_missing", "acct_missing"],
   6280     );
   6281     assert_required_approval_token_rejected(&sandbox, "order.accept", &["order", "accept"]);
   6282     assert_required_approval_token_rejected(
   6283         &sandbox,
   6284         "order.decline",
   6285         &["order", "decline", "--reason", "out_of_stock"],
   6286     );
   6287     assert_required_approval_token_rejected(
   6288         &sandbox,
   6289         "order.cancel",
   6290         &["order", "cancel", "--reason", "changed plans"],
   6291     );
   6292     assert_required_approval_token_rejected(
   6293         &sandbox,
   6294         "order.revision.accept",
   6295         &[
   6296             "order",
   6297             "revision",
   6298             "accept",
   6299             "ord_pending",
   6300             "--revision-id",
   6301             "rev_pending",
   6302         ],
   6303     );
   6304     assert_required_approval_token_rejected(
   6305         &sandbox,
   6306         "order.revision.decline",
   6307         &[
   6308             "order",
   6309             "revision",
   6310             "decline",
   6311             "ord_pending",
   6312             "--revision-id",
   6313             "rev_pending",
   6314             "--reason",
   6315             "keep original order",
   6316         ],
   6317     );
   6318 }
   6319 
   6320 fn assert_required_approval_token_rejected(
   6321     sandbox: &RadrootsCliSandbox,
   6322     operation_id: &str,
   6323     command_args: &[&str],
   6324 ) {
   6325     for token in [None, Some(""), Some(" \t ")] {
   6326         let mut args = vec!["--format", "json"];
   6327         if let Some(token) = token {
   6328             args.push("--approval-token");
   6329             args.push(token);
   6330         }
   6331         args.extend_from_slice(command_args);
   6332 
   6333         let (output, value) = sandbox.json_output(&args);
   6334 
   6335         assert_eq!(output.status.code(), Some(6), "`{args:?}` should fail");
   6336         assert_eq!(value["operation_id"], operation_id);
   6337         assert_eq!(value["errors"][0]["code"], "approval_required");
   6338         assert_eq!(value["errors"][0]["exit_code"], 6);
   6339         assert_no_removed_command_reference(&value, &args);
   6340     }
   6341 }
   6342 
   6343 #[test]
   6344 fn order_submit_missing_order_returns_not_found_while_read_view_stays_successful() {
   6345     let sandbox = RadrootsCliSandbox::new();
   6346 
   6347     let get = sandbox.json_success(&[
   6348         "--format",
   6349         "json",
   6350         "order",
   6351         "get",
   6352         "ord_missing_submit_target",
   6353     ]);
   6354     assert_eq!(get["operation_id"], "order.get");
   6355     assert_eq!(get["result"]["state"], "missing");
   6356     assert_eq!(get["errors"].as_array().expect("errors").len(), 0);
   6357 
   6358     let (output, submit) = sandbox.json_output(&[
   6359         "--format",
   6360         "json",
   6361         "--approval-token",
   6362         "approve",
   6363         "order",
   6364         "submit",
   6365         "ord_missing_submit_target",
   6366     ]);
   6367 
   6368     assert_eq!(output.status.code(), Some(4));
   6369     assert_eq!(submit["operation_id"], "order.submit");
   6370     assert_eq!(submit["errors"][0]["code"], "not_found");
   6371     assert_eq!(submit["errors"][0]["exit_code"], 4);
   6372     assert_eq!(submit["errors"][0]["detail"]["class"], "resource");
   6373     assert_no_removed_command_reference(&submit, &["order", "submit"]);
   6374     assert_no_daemon_runtime_reference(&submit, &["order", "submit"]);
   6375 }
   6376 
   6377 fn create_ready_order(sandbox: &RadrootsCliSandbox, basket_id: &str) -> String {
   6378     sandbox.json_success(&["--format", "json", "account", "create"]);
   6379     seed_orderable_listing(sandbox, LISTING_ADDR);
   6380     sandbox.json_success(&["--format", "json", "basket", "create", basket_id]);
   6381     sandbox.json_success(&[
   6382         "--format",
   6383         "json",
   6384         "basket",
   6385         "item",
   6386         "add",
   6387         basket_id,
   6388         "--listing-addr",
   6389         LISTING_ADDR,
   6390         "--bin-id",
   6391         "bin-1",
   6392         "--quantity",
   6393         "2",
   6394     ]);
   6395     let quote = sandbox.json_success(&["--format", "json", "basket", "quote", "create", basket_id]);
   6396     quote["result"]["quote"]["order_id"]
   6397         .as_str()
   6398         .expect("order id")
   6399         .to_owned()
   6400 }
   6401 
   6402 fn rewrite_order_bin(sandbox: &RadrootsCliSandbox, order_id: &str, bin_id: &str) {
   6403     let path = sandbox
   6404         .root()
   6405         .join("data/apps/cli/orders/drafts")
   6406         .join(format!("{order_id}.toml"));
   6407     let contents = fs::read_to_string(&path).expect("read order draft");
   6408     let updated = contents.replace(
   6409         "bin_id = \"bin-1\"",
   6410         format!("bin_id = \"{bin_id}\"").as_str(),
   6411     );
   6412     assert_ne!(updated, contents);
   6413     fs::write(path, updated).expect("rewrite order draft bin");
   6414 }
   6415 
   6416 fn rewrite_order_buyer_actor_pubkey(sandbox: &RadrootsCliSandbox, order_id: &str, pubkey: &str) {
   6417     let path = sandbox
   6418         .root()
   6419         .join("data/apps/cli/orders/drafts")
   6420         .join(format!("{order_id}.toml"));
   6421     let contents = fs::read_to_string(&path).expect("read order draft");
   6422     let mut in_buyer_actor = false;
   6423     let mut replaced = false;
   6424     let updated = contents
   6425         .lines()
   6426         .map(|line| {
   6427             let trimmed = line.trim_start();
   6428             if trimmed.starts_with('[') {
   6429                 in_buyer_actor = trimmed == "[buyer_actor]";
   6430             }
   6431             if in_buyer_actor && trimmed.starts_with("pubkey =") {
   6432                 replaced = true;
   6433                 format!("{}pubkey = \"{}\"", line_indent(line), pubkey)
   6434             } else {
   6435                 line.to_owned()
   6436             }
   6437         })
   6438         .collect::<Vec<_>>()
   6439         .join("\n");
   6440     assert!(replaced, "buyer_actor pubkey field");
   6441     fs::write(path, format!("{updated}\n")).expect("rewrite order draft buyer actor");
   6442 }
   6443 
   6444 fn line_indent(line: &str) -> &str {
   6445     let trimmed = line.trim_start();
   6446     &line[..line.len() - trimmed.len()]
   6447 }
   6448 
   6449 fn signed_order_request_event_for_quote(
   6450     buyer: &radroots_identity::RadrootsIdentity,
   6451     order_id: &str,
   6452     listing_event_id: &str,
   6453     economics: RadrootsOrderEconomics,
   6454 ) -> RadrootsNostrEvent {
   6455     let buyer_pubkey = buyer.public_key_hex();
   6456     let seller_pubkey = "1".repeat(64);
   6457     let payload = RadrootsOrderRequest {
   6458         order_id: test_order_id(order_id),
   6459         listing_addr: test_listing_addr(LISTING_ADDR),
   6460         buyer_pubkey: test_pubkey(buyer_pubkey.as_str()),
   6461         seller_pubkey: test_pubkey(seller_pubkey.as_str()),
   6462         items: vec![RadrootsOrderItem {
   6463             bin_id: test_inventory_bin_id("bin-1"),
   6464             bin_count: 2,
   6465         }],
   6466         economics,
   6467     };
   6468     let parts = order_request_event_build(
   6469         &RadrootsNostrEventPtr {
   6470             id: listing_event_id.to_owned(),
   6471             relays: None,
   6472         },
   6473         &payload,
   6474     )
   6475     .expect("order request parts");
   6476     radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
   6477         .expect("nostr event builder")
   6478         .sign_with_keys(buyer.keys())
   6479         .expect("signed order request")
   6480 }
   6481 
   6482 #[test]
   6483 fn buyer_target_flow_acceptance_uses_target_operations() {
   6484     let sandbox = RadrootsCliSandbox::new();
   6485 
   6486     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   6487     let account_id = account["result"]["account"]["id"]
   6488         .as_str()
   6489         .expect("account id");
   6490     assert_eq!(account["operation_id"], "account.create");
   6491     assert_eq!(account["result"]["account"]["signer"], "local");
   6492     assert_eq!(account["result"]["account"]["custody"], "secret_backed");
   6493     assert_eq!(account["result"]["account"]["write_capable"], true);
   6494     assert_no_removed_command_reference(&account, &["account", "create"]);
   6495 
   6496     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   6497     assert_eq!(signer["operation_id"], "signer.status.get");
   6498     assert_eq!(signer["result"]["mode"], "local");
   6499     assert_eq!(signer["result"]["state"], "ready");
   6500     assert_eq!(signer["result"]["signer_account_id"], account_id);
   6501     assert_no_removed_command_reference(&signer, &["signer", "status", "get"]);
   6502 
   6503     let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
   6504 
   6505     let search = sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
   6506     assert_eq!(search["operation_id"], "market.product.search");
   6507     assert_eq!(search["errors"].as_array().expect("errors").len(), 0);
   6508     assert_no_removed_command_reference(&search, &["market", "product", "search"]);
   6509 
   6510     let create = sandbox.json_success(&["--format", "json", "basket", "create", "basket_flow"]);
   6511     assert_eq!(create["operation_id"], "basket.create");
   6512     assert_eq!(create["result"]["basket_id"], "basket_flow");
   6513     assert_no_removed_command_reference(&create, &["basket", "create"]);
   6514 
   6515     let add = sandbox.json_success(&[
   6516         "--format",
   6517         "json",
   6518         "basket",
   6519         "item",
   6520         "add",
   6521         "basket_flow",
   6522         "--listing-addr",
   6523         LISTING_ADDR,
   6524         "--bin-id",
   6525         "bin-1",
   6526         "--quantity",
   6527         "2",
   6528     ]);
   6529     assert_eq!(add["operation_id"], "basket.item.add");
   6530     assert_eq!(add["result"]["ready_for_quote"], true);
   6531     assert_no_removed_command_reference(&add, &["basket", "item", "add"]);
   6532 
   6533     let quote = sandbox.json_success(&[
   6534         "--format",
   6535         "json",
   6536         "basket",
   6537         "quote",
   6538         "create",
   6539         "basket_flow",
   6540     ]);
   6541     assert_eq!(quote["operation_id"], "basket.quote.create");
   6542     assert_eq!(quote["result"]["state"], "quoted");
   6543     assert_no_removed_command_reference(&quote, &["basket", "quote", "create"]);
   6544     let order_id = quote["result"]["quote"]["order_id"]
   6545         .as_str()
   6546         .expect("order id");
   6547     let quote_economics = &quote["result"]["quote"]["economics"];
   6548     let order_file = quote["result"]["order"]["file"]
   6549         .as_str()
   6550         .expect("order file");
   6551     assert_eq!(quote["result"]["quote"]["ready_for_submit"], true);
   6552     assert_eq!(quote["result"]["quote"]["quote_version"], 1);
   6553     assert_eq!(
   6554         quote["result"]["quote"]["quote_id"],
   6555         quote_economics["quote_id"]
   6556     );
   6557     assert_eq!(quote_economics["quote_version"], 1);
   6558     assert_eq!(quote_economics["pricing_basis"], "listing_event");
   6559     assert_eq!(quote_economics["currency"], "USD");
   6560     assert_eq!(quote_economics["items"][0]["bin_id"], "bin-1");
   6561     assert_eq!(quote_economics["items"][0]["bin_count"], 2);
   6562     assert_eq!(quote_economics["discounts"], Value::Array(Vec::new()));
   6563     assert_eq!(quote_economics["adjustments"], Value::Array(Vec::new()));
   6564     assert_eq!(
   6565         quote["result"]["order"]["economics"],
   6566         quote_economics.clone()
   6567     );
   6568     let order_draft = fs::read_to_string(order_file).expect("read order draft");
   6569     assert!(order_draft.contains("[buyer_actor]"));
   6570     assert!(order_draft.contains("source = \"resolved_account\""));
   6571     assert!(order_draft.contains("[order.economics]"));
   6572     assert!(order_draft.contains("pricing_basis = \"listing_event\""));
   6573     assert_eq!(quote["result"]["order"]["buyer_account_id"], account_id);
   6574     assert_eq!(
   6575         quote["result"]["order"]["buyer_actor_source"],
   6576         "resolved_account"
   6577     );
   6578     assert_eq!(
   6579         quote["result"]["order"]["listing_event_id"],
   6580         listing_event_id
   6581     );
   6582 
   6583     let orders = sandbox.json_success(&["--format", "json", "order", "list"]);
   6584     assert_eq!(orders["operation_id"], "order.list");
   6585     assert_eq!(orders["result"]["state"], "ready");
   6586     assert_eq!(orders["result"]["count"], 1);
   6587     assert_eq!(orders["result"]["orders"][0]["id"], order_id);
   6588     assert_eq!(orders["result"]["orders"][0]["ready_for_submit"], true);
   6589     assert_eq!(
   6590         orders["result"]["orders"][0]["listing_event_id"],
   6591         listing_event_id
   6592     );
   6593     assert_eq!(
   6594         orders["result"]["orders"][0]["buyer_account_id"],
   6595         account_id
   6596     );
   6597     assert_eq!(
   6598         orders["result"]["orders"][0]["buyer_actor_source"],
   6599         "resolved_account"
   6600     );
   6601     assert_eq!(
   6602         orders["result"]["orders"][0]["economics"],
   6603         quote_economics.clone()
   6604     );
   6605     assert_eq!(orders["result"]["orders"][0]["issues"], Value::Null);
   6606     assert_no_removed_command_reference(&orders, &["order", "list"]);
   6607     assert_no_daemon_runtime_reference(&orders, &["order", "list"]);
   6608 
   6609     let submit =
   6610         sandbox.json_success(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   6611     assert_eq!(submit["operation_id"], "order.submit");
   6612     assert_eq!(submit["dry_run"], true);
   6613     assert_eq!(submit["result"]["state"], "dry_run");
   6614     assert_eq!(submit["result"]["source"], "SDK order submit ยท local key");
   6615     assert_eq!(submit["result"]["event_kind"], 3422);
   6616     assert_eq!(
   6617         submit["result"]["target_relays"][0],
   6618         ORDERABLE_LISTING_RELAY
   6619     );
   6620     assert_eq!(
   6621         submit["result"]["event_id"]
   6622             .as_str()
   6623             .expect("event id")
   6624             .len(),
   6625         64
   6626     );
   6627     assert_no_removed_command_reference(&submit, &["order", "submit", "--dry-run"]);
   6628     assert_no_daemon_runtime_reference(&submit, &["order", "submit", "--dry-run"]);
   6629 
   6630     let (output, unavailable_submit) = sandbox.json_output(&[
   6631         "--format",
   6632         "json",
   6633         "--approval-token",
   6634         "approve",
   6635         "order",
   6636         "submit",
   6637         order_id,
   6638     ]);
   6639     assert!(!output.status.success());
   6640     assert_eq!(output.status.code(), Some(3), "{unavailable_submit}");
   6641     assert_eq!(unavailable_submit["operation_id"], "order.submit");
   6642     assert_eq!(unavailable_submit["result"], Value::Null);
   6643     assert_eq!(
   6644         unavailable_submit["errors"][0]["code"],
   6645         "operation_unavailable"
   6646     );
   6647     assert_eq!(
   6648         unavailable_submit["errors"][0]["detail"]["class"],
   6649         "operation"
   6650     );
   6651     assert_eq!(
   6652         unavailable_submit["errors"][0]["detail"]["state"],
   6653         "unavailable"
   6654     );
   6655     assert!(
   6656         unavailable_submit["errors"][0]["message"]
   6657             .as_str()
   6658             .expect("message")
   6659             .contains("SDK relay publish")
   6660     );
   6661     assert_no_removed_command_reference(&unavailable_submit, &["order", "submit"]);
   6662     assert_no_daemon_runtime_reference(&unavailable_submit, &["order", "submit"]);
   6663 
   6664     let order_after_submit = sandbox.json_success(&["--format", "json", "order", "get", order_id]);
   6665     assert_eq!(order_after_submit["operation_id"], "order.get");
   6666     assert_eq!(order_after_submit["result"]["state"], "ready");
   6667     assert_eq!(
   6668         order_after_submit["result"]["economics"],
   6669         quote_economics.clone()
   6670     );
   6671     assert_eq!(order_after_submit["result"]["job"], Value::Null);
   6672     assert_eq!(order_after_submit["result"]["workflow"], Value::Null);
   6673     assert_no_daemon_runtime_reference(&order_after_submit, &["order", "get"]);
   6674 
   6675     let (watch_output, watch) =
   6676         sandbox.json_output(&["--format", "json", "order", "event", "watch", order_id]);
   6677     assert!(!watch_output.status.success());
   6678     assert_eq!(watch_output.status.code(), Some(3));
   6679     assert_eq!(watch["operation_id"], "order.event.watch");
   6680     assert_eq!(watch["result"], Value::Null);
   6681     assert_eq!(watch["errors"][0]["code"], "not_implemented");
   6682     assert_eq!(watch["errors"][0]["detail"]["state"], "not_implemented");
   6683     assert_eq!(watch["errors"][0]["detail"]["order_id"], order_id);
   6684     assert_eq!(
   6685         watch["next_actions"][0]["command"],
   6686         format!("radroots order status get {order_id}")
   6687     );
   6688     assert_no_daemon_runtime_reference(&watch, &["order", "event", "watch"]);
   6689     assert!(
   6690         !serde_json::to_string(&watch)
   6691             .expect("watch json")
   6692             .contains("local order drafts")
   6693     );
   6694 }
   6695 
   6696 #[test]
   6697 fn order_get_and_list_report_missing_bound_buyer_account() {
   6698     let sandbox = RadrootsCliSandbox::new();
   6699     let order_id = create_ready_order(&sandbox, "missing_buyer_account");
   6700 
   6701     let ready = sandbox.json_success(&["--format", "json", "order", "get", order_id.as_str()]);
   6702     let account_id = ready["result"]["buyer_account_id"]
   6703         .as_str()
   6704         .expect("buyer account id");
   6705     assert_eq!(ready["result"]["state"], "ready");
   6706     assert_eq!(ready["result"]["buyer_account_id"], account_id);
   6707     assert_eq!(ready["result"]["buyer_custody"], "secret_backed");
   6708     assert_eq!(ready["result"]["buyer_write_capable"], true);
   6709 
   6710     sandbox.json_success(&[
   6711         "--format",
   6712         "json",
   6713         "--approval-token",
   6714         "approve",
   6715         "account",
   6716         "remove",
   6717         account_id,
   6718     ]);
   6719 
   6720     let missing = sandbox.json_success(&["--format", "json", "order", "get", order_id.as_str()]);
   6721     assert_eq!(missing["operation_id"], "order.get");
   6722     assert_eq!(missing["result"]["state"], "draft");
   6723     assert_eq!(missing["result"]["ready_for_submit"], false);
   6724     assert_eq!(missing["result"]["buyer_account_id"], account_id);
   6725     assert_eq!(missing["result"]["buyer_custody"], Value::Null);
   6726     assert!(
   6727         missing["result"]["issues"]
   6728             .as_array()
   6729             .expect("issues")
   6730             .iter()
   6731             .any(|issue| issue["code"] == "account_unresolved")
   6732     );
   6733     assert!(
   6734         missing["result"]["actions"]
   6735             .as_array()
   6736             .expect("actions")
   6737             .iter()
   6738             .any(|action| action == "radroots account import <path>")
   6739     );
   6740     assert!(
   6741         missing["result"]["actions"]
   6742             .as_array()
   6743             .expect("actions")
   6744             .iter()
   6745             .any(|action| action
   6746                 == &Value::String(format!("radroots order rebind {order_id} <selector>")))
   6747     );
   6748 
   6749     let list = sandbox.json_success(&["--format", "json", "order", "list"]);
   6750     assert_eq!(list["result"]["state"], "degraded");
   6751     assert_eq!(list["result"]["orders"][0]["ready_for_submit"], false);
   6752     assert!(
   6753         list["result"]["orders"][0]["issues"]
   6754             .as_array()
   6755             .expect("issues")
   6756             .iter()
   6757             .any(|issue| issue["code"] == "account_unresolved")
   6758     );
   6759 
   6760     let (submit_output, submit) = sandbox.json_output(&[
   6761         "--format",
   6762         "json",
   6763         "--dry-run",
   6764         "order",
   6765         "submit",
   6766         order_id.as_str(),
   6767     ]);
   6768     assert!(!submit_output.status.success());
   6769     assert_eq!(submit_output.status.code(), Some(5));
   6770     assert_eq!(submit["operation_id"], "order.submit");
   6771     assert_eq!(submit["errors"][0]["code"], "account_unresolved");
   6772     assert_eq!(submit["errors"][0]["detail"]["order_id"], order_id);
   6773 }
   6774 
   6775 #[test]
   6776 fn order_get_marks_watch_only_bound_buyer_unready() {
   6777     let sandbox = RadrootsCliSandbox::new();
   6778     let public_identity = identity_public(92);
   6779     let public_identity_file =
   6780         write_public_identity_profile(&sandbox, "order-watch-only-buyer", &public_identity);
   6781     let imported = sandbox.json_success(&[
   6782         "--format",
   6783         "json",
   6784         "--approval-token",
   6785         "approve",
   6786         "account",
   6787         "import",
   6788         "--default",
   6789         public_identity_file.to_string_lossy().as_ref(),
   6790     ]);
   6791     let account_id = imported["result"]["account"]["id"]
   6792         .as_str()
   6793         .expect("watch account id");
   6794     assert_eq!(imported["result"]["account"]["custody"], "watch_only");
   6795 
   6796     seed_orderable_listing(&sandbox, LISTING_ADDR);
   6797     sandbox.json_success(&["--format", "json", "basket", "create", "watch_buyer"]);
   6798     sandbox.json_success(&[
   6799         "--format",
   6800         "json",
   6801         "basket",
   6802         "item",
   6803         "add",
   6804         "watch_buyer",
   6805         "--listing-addr",
   6806         LISTING_ADDR,
   6807         "--bin-id",
   6808         "bin-1",
   6809         "--quantity",
   6810         "2",
   6811     ]);
   6812     let quote = sandbox.json_success(&[
   6813         "--format",
   6814         "json",
   6815         "basket",
   6816         "quote",
   6817         "create",
   6818         "watch_buyer",
   6819     ]);
   6820     let order_id = quote["result"]["quote"]["order_id"]
   6821         .as_str()
   6822         .expect("order id");
   6823 
   6824     let get = sandbox.json_success(&["--format", "json", "order", "get", order_id]);
   6825     assert_eq!(get["result"]["state"], "draft");
   6826     assert_eq!(get["result"]["ready_for_submit"], false);
   6827     assert_eq!(get["result"]["buyer_account_id"], account_id);
   6828     assert_eq!(get["result"]["buyer_custody"], "watch_only");
   6829     assert_eq!(get["result"]["buyer_write_capable"], false);
   6830     assert!(
   6831         get["result"]["issues"]
   6832             .as_array()
   6833             .expect("issues")
   6834             .iter()
   6835             .any(|issue| issue["code"] == "account_watch_only")
   6836     );
   6837     assert!(
   6838         get["result"]["actions"]
   6839             .as_array()
   6840             .expect("actions")
   6841             .iter()
   6842             .any(|action| action
   6843                 == &Value::String(format!(
   6844                     "radroots account attach-secret {account_id} <path>"
   6845                 )))
   6846     );
   6847 
   6848     let (submit_output, submit) =
   6849         sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   6850     assert!(!submit_output.status.success());
   6851     assert_eq!(submit_output.status.code(), Some(7));
   6852     assert_eq!(submit["operation_id"], "order.submit");
   6853     assert_eq!(submit["errors"][0]["code"], "account_watch_only");
   6854     assert_eq!(
   6855         submit["errors"][0]["detail"]["order_buyer_account_id"],
   6856         account_id
   6857     );
   6858 }
   6859 
   6860 #[test]
   6861 fn order_get_marks_bound_buyer_pubkey_mismatch_unready() {
   6862     let sandbox = RadrootsCliSandbox::new();
   6863     let order_id = create_ready_order(&sandbox, "mismatched_buyer_actor");
   6864     let other_pubkey = identity_public(93).public_key_hex;
   6865     rewrite_order_buyer_actor_pubkey(&sandbox, order_id.as_str(), other_pubkey.as_str());
   6866 
   6867     let get = sandbox.json_success(&["--format", "json", "order", "get", order_id.as_str()]);
   6868     assert_eq!(get["result"]["state"], "draft");
   6869     assert_eq!(get["result"]["ready_for_submit"], false);
   6870     assert_eq!(get["result"]["buyer_custody"], "secret_backed");
   6871     assert_eq!(get["result"]["buyer_write_capable"], true);
   6872     assert!(
   6873         get["result"]["issues"]
   6874             .as_array()
   6875             .expect("issues")
   6876             .iter()
   6877             .any(|issue| issue["code"] == "account_mismatch")
   6878     );
   6879     assert!(
   6880         get["result"]["actions"]
   6881             .as_array()
   6882             .expect("actions")
   6883             .iter()
   6884             .any(|action| action
   6885                 == &Value::String(format!("radroots order rebind {order_id} <selector>")))
   6886     );
   6887 }
   6888 
   6889 #[test]
   6890 fn order_rebind_previews_and_writes_bound_buyer_actor_updates() {
   6891     let sandbox = RadrootsCliSandbox::new();
   6892     let order_id = create_ready_order(&sandbox, "order_rebind");
   6893     let order_file = sandbox
   6894         .root()
   6895         .join("data/apps/cli/orders/drafts")
   6896         .join(format!("{order_id}.toml"));
   6897     let before = fs::read_to_string(&order_file).expect("order before rebind");
   6898     let second = sandbox.json_success(&["--format", "json", "account", "create"]);
   6899     let second_account_id = second["result"]["account"]["id"]
   6900         .as_str()
   6901         .expect("second account id");
   6902 
   6903     let dry_run = sandbox.json_success(&[
   6904         "--format",
   6905         "json",
   6906         "--dry-run",
   6907         "order",
   6908         "rebind",
   6909         order_id.as_str(),
   6910         second_account_id,
   6911     ]);
   6912     assert_eq!(dry_run["operation_id"], "order.rebind");
   6913     assert_eq!(dry_run["result"]["state"], "dry_run");
   6914     assert_eq!(dry_run["result"]["from_order_id"], order_id);
   6915     assert_eq!(dry_run["result"]["order_id_changed"], true);
   6916     assert_eq!(dry_run["result"]["buyer_pubkey_changed"], true);
   6917     assert_eq!(dry_run["result"]["to_buyer_account_id"], second_account_id);
   6918     assert_eq!(
   6919         dry_run["result"]["existing_request_check"],
   6920         "skipped_no_relays"
   6921     );
   6922     assert_eq!(
   6923         fs::read_to_string(&order_file).expect("order after dry-run rebind"),
   6924         before
   6925     );
   6926 
   6927     let (unapproved_output, unapproved) = sandbox.json_output(&[
   6928         "--format",
   6929         "json",
   6930         "order",
   6931         "rebind",
   6932         order_id.as_str(),
   6933         second_account_id,
   6934     ]);
   6935     assert!(!unapproved_output.status.success());
   6936     assert_eq!(unapproved["operation_id"], "order.rebind");
   6937     assert_eq!(unapproved["errors"][0]["code"], "approval_required");
   6938 
   6939     let rebound = sandbox.json_success(&[
   6940         "--format",
   6941         "json",
   6942         "--approval-token",
   6943         "approve",
   6944         "order",
   6945         "rebind",
   6946         order_id.as_str(),
   6947         second_account_id,
   6948     ]);
   6949     assert_eq!(rebound["operation_id"], "order.rebind");
   6950     assert_eq!(rebound["result"]["state"], "rebound");
   6951     assert_eq!(rebound["result"]["from_order_id"], order_id);
   6952     assert_eq!(rebound["result"]["order_id_changed"], true);
   6953     let rebound_order_id = rebound["result"]["to_order_id"]
   6954         .as_str()
   6955         .expect("rebound order id");
   6956     assert_ne!(rebound_order_id, order_id);
   6957     let rebound_file = rebound["result"]["file"].as_str().expect("rebound file");
   6958     assert!(!order_file.exists());
   6959     let after = fs::read_to_string(rebound_file).expect("order after rebind");
   6960     assert!(after.contains("[buyer_actor]"));
   6961     assert!(after.contains("source = \"order_rebind\""));
   6962     assert!(after.contains(format!("order_id = \"{rebound_order_id}\"").as_str()));
   6963     assert!(after.contains(format!("quote_id = \"quote_{rebound_order_id}\"").as_str()));
   6964 
   6965     let get = sandbox.json_success(&["--format", "json", "order", "get", rebound_order_id]);
   6966     assert_eq!(get["result"]["state"], "ready");
   6967     assert_eq!(get["result"]["buyer_account_id"], second_account_id);
   6968     assert_eq!(get["result"]["buyer_actor_source"], "order_rebind");
   6969 
   6970     let same = sandbox.json_success(&[
   6971         "--format",
   6972         "json",
   6973         "--approval-token",
   6974         "approve",
   6975         "order",
   6976         "rebind",
   6977         rebound_order_id,
   6978         second_account_id,
   6979     ]);
   6980     assert_eq!(same["result"]["state"], "rebound");
   6981     assert_eq!(same["result"]["order_id_changed"], false);
   6982     assert_eq!(same["result"]["to_order_id"], rebound_order_id);
   6983 }
   6984 
   6985 #[test]
   6986 fn order_rebind_refuses_visible_published_request() {
   6987     let sandbox = RadrootsCliSandbox::new();
   6988     let buyer = identity_secret(94);
   6989     let buyer_public = buyer.to_public();
   6990     let buyer_public_file =
   6991         write_public_identity_profile(&sandbox, "rebind-visible-buyer", &buyer_public);
   6992     sandbox.json_success(&[
   6993         "--format",
   6994         "json",
   6995         "--approval-token",
   6996         "approve",
   6997         "account",
   6998         "import",
   6999         "--default",
   7000         buyer_public_file.to_string_lossy().as_ref(),
   7001     ]);
   7002     let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
   7003     sandbox.json_success(&["--format", "json", "basket", "create", "visible_rebind"]);
   7004     sandbox.json_success(&[
   7005         "--format",
   7006         "json",
   7007         "basket",
   7008         "item",
   7009         "add",
   7010         "visible_rebind",
   7011         "--listing-addr",
   7012         LISTING_ADDR,
   7013         "--bin-id",
   7014         "bin-1",
   7015         "--quantity",
   7016         "2",
   7017     ]);
   7018     let quote = sandbox.json_success(&[
   7019         "--format",
   7020         "json",
   7021         "basket",
   7022         "quote",
   7023         "create",
   7024         "visible_rebind",
   7025     ]);
   7026     let order_id = quote["result"]["quote"]["order_id"]
   7027         .as_str()
   7028         .expect("order id");
   7029     let economics: RadrootsOrderEconomics =
   7030         serde_json::from_value(quote["result"]["quote"]["economics"].clone())
   7031             .expect("quote economics");
   7032     let event = signed_order_request_event_for_quote(
   7033         &buyer,
   7034         order_id,
   7035         listing_event_id.as_str(),
   7036         economics,
   7037     );
   7038     let target = sandbox.json_success(&["--format", "json", "account", "create"]);
   7039     let target_account_id = target["result"]["account"]["id"]
   7040         .as_str()
   7041         .expect("target account id");
   7042     let relay = RelayFetchServer::with_events(vec![event]);
   7043 
   7044     let (output, value) = sandbox.json_output(&[
   7045         "--format",
   7046         "json",
   7047         "--dry-run",
   7048         "--relay",
   7049         relay.endpoint(),
   7050         "order",
   7051         "rebind",
   7052         order_id,
   7053         target_account_id,
   7054     ]);
   7055     relay.join();
   7056 
   7057     assert!(!output.status.success());
   7058     assert_eq!(output.status.code(), Some(10));
   7059     assert_eq!(value["operation_id"], "order.rebind");
   7060     assert_eq!(value["errors"][0]["code"], "validation_failed");
   7061     assert_eq!(
   7062         value["errors"][0]["detail"]["existing_request_check"],
   7063         "blocked_existing_request"
   7064     );
   7065     assert_eq!(
   7066         value["errors"][0]["detail"]["existing_request_event_ids"]
   7067             .as_array()
   7068             .expect("existing request ids")
   7069             .len(),
   7070         1
   7071     );
   7072 }
   7073 
   7074 #[test]
   7075 fn order_status_and_event_list_use_draft_context_after_account_override_drift() {
   7076     let sandbox = RadrootsCliSandbox::new();
   7077     let buyer = identity_secret(95);
   7078     let buyer_public_file =
   7079         write_public_identity_profile(&sandbox, "status-draft-buyer", &buyer.to_public());
   7080     sandbox.json_success(&[
   7081         "--format",
   7082         "json",
   7083         "--approval-token",
   7084         "approve",
   7085         "account",
   7086         "import",
   7087         "--default",
   7088         buyer_public_file.to_string_lossy().as_ref(),
   7089     ]);
   7090     let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
   7091     sandbox.json_success(&["--format", "json", "basket", "create", "draft_status"]);
   7092     sandbox.json_success(&[
   7093         "--format",
   7094         "json",
   7095         "basket",
   7096         "item",
   7097         "add",
   7098         "draft_status",
   7099         "--listing-addr",
   7100         LISTING_ADDR,
   7101         "--bin-id",
   7102         "bin-1",
   7103         "--quantity",
   7104         "2",
   7105     ]);
   7106     let quote = sandbox.json_success(&[
   7107         "--format",
   7108         "json",
   7109         "basket",
   7110         "quote",
   7111         "create",
   7112         "draft_status",
   7113     ]);
   7114     let order_id = quote["result"]["quote"]["order_id"]
   7115         .as_str()
   7116         .expect("order id");
   7117     let economics: RadrootsOrderEconomics =
   7118         serde_json::from_value(quote["result"]["quote"]["economics"].clone())
   7119             .expect("quote economics");
   7120     let event = signed_order_request_event_for_quote(
   7121         &buyer,
   7122         order_id,
   7123         listing_event_id.as_str(),
   7124         economics,
   7125     );
   7126     let drift_account = sandbox.json_success(&["--format", "json", "account", "create"]);
   7127     let drift_account_id = drift_account["result"]["account"]["id"]
   7128         .as_str()
   7129         .expect("drift account id");
   7130 
   7131     let status = sandbox.json_success(&[
   7132         "--format",
   7133         "json",
   7134         "--account-id",
   7135         drift_account_id,
   7136         "order",
   7137         "status",
   7138         "get",
   7139         order_id,
   7140     ]);
   7141 
   7142     assert_eq!(status["operation_id"], "order.status.get");
   7143     assert_eq!(status["result"]["source"], "SDK local order projection");
   7144     assert_eq!(
   7145         status["result"]["actor_context_source"],
   7146         "sdk_local_projection"
   7147     );
   7148     assert_eq!(status["result"]["state"], "missing");
   7149     assert_eq!(status["result"]["fetched_count"], 0);
   7150     assert_eq!(status["result"]["decoded_count"], 0);
   7151 
   7152     let event_list_relay = RelayFetchServer::with_events(vec![event]);
   7153     let events = sandbox.json_success(&[
   7154         "--format",
   7155         "json",
   7156         "--account-id",
   7157         drift_account_id,
   7158         "--relay",
   7159         event_list_relay.endpoint(),
   7160         "order",
   7161         "event",
   7162         "list",
   7163         order_id,
   7164     ]);
   7165     event_list_relay.join();
   7166 
   7167     assert_eq!(events["operation_id"], "order.event.list");
   7168     assert_eq!(events["result"]["actor_context_source"], "order_draft");
   7169     assert_eq!(events["result"]["seller_pubkey"], "1".repeat(64));
   7170     assert_eq!(events["result"]["count"], 1);
   7171     assert_eq!(events["result"]["orders"][0]["id"], order_id);
   7172 }
   7173 
   7174 #[test]
   7175 fn order_cancel_uses_bound_buyer_after_default_account_drift() {
   7176     let sandbox = RadrootsCliSandbox::new();
   7177     let buyer = identity_secret(96);
   7178     let buyer_public_file =
   7179         write_public_identity_profile(&sandbox, "cancel-bound-buyer", &buyer.to_public());
   7180     let imported = sandbox.json_success(&[
   7181         "--format",
   7182         "json",
   7183         "--approval-token",
   7184         "approve",
   7185         "account",
   7186         "import",
   7187         "--default",
   7188         buyer_public_file.to_string_lossy().as_ref(),
   7189     ]);
   7190     let buyer_account_id = imported["result"]["account"]["id"]
   7191         .as_str()
   7192         .expect("buyer account id");
   7193     let buyer_secret_file = write_secret_identity_profile(&sandbox, "cancel-bound-secret", &buyer);
   7194     sandbox.json_success(&[
   7195         "--format",
   7196         "json",
   7197         "--approval-token",
   7198         "approve",
   7199         "account",
   7200         "attach-secret",
   7201         buyer_account_id,
   7202         buyer_secret_file.to_string_lossy().as_ref(),
   7203         "--default",
   7204     ]);
   7205     let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
   7206     sandbox.json_success(&["--format", "json", "basket", "create", "bound_cancel"]);
   7207     sandbox.json_success(&[
   7208         "--format",
   7209         "json",
   7210         "basket",
   7211         "item",
   7212         "add",
   7213         "bound_cancel",
   7214         "--listing-addr",
   7215         LISTING_ADDR,
   7216         "--bin-id",
   7217         "bin-1",
   7218         "--quantity",
   7219         "2",
   7220     ]);
   7221     let quote = sandbox.json_success(&[
   7222         "--format",
   7223         "json",
   7224         "basket",
   7225         "quote",
   7226         "create",
   7227         "bound_cancel",
   7228     ]);
   7229     let order_id = quote["result"]["quote"]["order_id"]
   7230         .as_str()
   7231         .expect("order id");
   7232     let economics: RadrootsOrderEconomics =
   7233         serde_json::from_value(quote["result"]["quote"]["economics"].clone())
   7234             .expect("quote economics");
   7235     let event = signed_order_request_event_for_quote(
   7236         &buyer,
   7237         order_id,
   7238         listing_event_id.as_str(),
   7239         economics,
   7240     );
   7241     let drift_account = sandbox.json_success(&["--format", "json", "account", "create"]);
   7242     let drift_account_id = drift_account["result"]["account"]["id"]
   7243         .as_str()
   7244         .expect("drift account id");
   7245     sandbox.json_success(&[
   7246         "--format",
   7247         "json",
   7248         "account",
   7249         "selection",
   7250         "update",
   7251         drift_account_id,
   7252     ]);
   7253     let relay = RelayFetchServer::with_events(vec![event]);
   7254 
   7255     let cancel = sandbox.json_success(&[
   7256         "--format",
   7257         "json",
   7258         "--dry-run",
   7259         "--relay",
   7260         relay.endpoint(),
   7261         "order",
   7262         "cancel",
   7263         order_id,
   7264         "--reason",
   7265         "changed plans",
   7266     ]);
   7267     relay.join();
   7268 
   7269     assert_eq!(cancel["operation_id"], "order.cancel");
   7270     assert_eq!(cancel["result"]["state"], "dry_run");
   7271     assert_eq!(cancel["result"]["buyer_pubkey"], buyer.public_key_hex());
   7272     assert_eq!(cancel["result"]["signer_mode"], "local");
   7273 }
   7274 
   7275 #[test]
   7276 fn buyer_side_order_writes_reject_conflicting_account_override_for_local_draft() {
   7277     let sandbox = RadrootsCliSandbox::new();
   7278     let order_id = create_ready_order(&sandbox, "buyer_write_drift");
   7279     let drift_account = sandbox.json_success(&["--format", "json", "account", "create"]);
   7280     let drift_account_id = drift_account["result"]["account"]["id"]
   7281         .as_str()
   7282         .expect("drift account id");
   7283 
   7284     for (operation_id, command) in [
   7285         (
   7286             "order.revision.accept",
   7287             vec![
   7288                 "--format",
   7289                 "json",
   7290                 "--dry-run",
   7291                 "--account-id",
   7292                 drift_account_id,
   7293                 "--relay",
   7294                 "ws://127.0.0.1:9",
   7295                 "order",
   7296                 "revision",
   7297                 "accept",
   7298                 order_id.as_str(),
   7299                 "--revision-id",
   7300                 "rev_pending",
   7301             ],
   7302         ),
   7303         (
   7304             "order.cancel",
   7305             vec![
   7306                 "--format",
   7307                 "json",
   7308                 "--dry-run",
   7309                 "--account-id",
   7310                 drift_account_id,
   7311                 "--relay",
   7312                 "ws://127.0.0.1:9",
   7313                 "order",
   7314                 "cancel",
   7315                 order_id.as_str(),
   7316                 "--reason",
   7317                 "changed plans",
   7318             ],
   7319         ),
   7320     ] {
   7321         let (output, value) = sandbox.json_output(command.as_slice());
   7322 
   7323         assert!(!output.status.success(), "{operation_id} should fail");
   7324         assert_eq!(output.status.code(), Some(5));
   7325         assert_eq!(value["operation_id"], operation_id);
   7326         assert_eq!(value["result"], Value::Null);
   7327         assert_eq!(value["errors"][0]["code"], "account_mismatch");
   7328         assert_eq!(value["errors"][0]["detail"]["order_id"], order_id);
   7329         assert_eq!(
   7330             value["errors"][0]["detail"]["attempted_buyer_account_id"],
   7331             drift_account_id
   7332         );
   7333     }
   7334 }
   7335 
   7336 #[test]
   7337 fn order_submit_requires_local_replica_freshness_before_signing() {
   7338     let sandbox = RadrootsCliSandbox::new();
   7339     let order_id = create_ready_order(&sandbox, "freshness_missing_db");
   7340     fs::remove_file(sandbox.replica_db_path()).expect("remove replica db");
   7341 
   7342     let (output, value) = sandbox.json_output(&[
   7343         "--format",
   7344         "json",
   7345         "--relay",
   7346         "ws://127.0.0.1:9",
   7347         "--approval-token",
   7348         "approve",
   7349         "order",
   7350         "submit",
   7351         order_id.as_str(),
   7352     ]);
   7353 
   7354     assert!(!output.status.success());
   7355     assert_eq!(output.status.code(), Some(3));
   7356     assert_eq!(value["operation_id"], "order.submit");
   7357     assert_eq!(value["errors"][0]["code"], "operation_unavailable");
   7358     assert_eq!(value["errors"][0]["detail"]["state"], "unconfigured");
   7359     assert_eq!(
   7360         value["errors"][0]["detail"]["issues"][0]["field"],
   7361         "order.listing_addr"
   7362     );
   7363     assert!(
   7364         value["errors"][0]["message"]
   7365             .as_str()
   7366             .expect("message")
   7367             .contains("run `radroots store init` and `radroots market refresh`")
   7368     );
   7369 }
   7370 
   7371 #[test]
   7372 fn order_submit_dry_run_requires_local_replica_freshness() {
   7373     let sandbox = RadrootsCliSandbox::new();
   7374     let order_id = create_ready_order(&sandbox, "dry_freshness_missing_db");
   7375     fs::remove_file(sandbox.replica_db_path()).expect("remove replica db");
   7376 
   7377     let (output, value) = sandbox.json_output(&[
   7378         "--format",
   7379         "json",
   7380         "--dry-run",
   7381         "order",
   7382         "submit",
   7383         order_id.as_str(),
   7384     ]);
   7385 
   7386     assert!(!output.status.success());
   7387     assert_eq!(output.status.code(), Some(3));
   7388     assert_eq!(value["operation_id"], "order.submit");
   7389     assert_eq!(value["dry_run"], true);
   7390     assert_eq!(value["errors"][0]["code"], "operation_unavailable");
   7391     assert_eq!(value["errors"][0]["detail"]["state"], "unconfigured");
   7392     assert_eq!(
   7393         value["errors"][0]["detail"]["issues"][0]["field"],
   7394         "order.listing_addr"
   7395     );
   7396 }
   7397 
   7398 #[test]
   7399 fn order_submit_rejects_missing_or_archived_local_listing_before_publish() {
   7400     let sandbox = RadrootsCliSandbox::new();
   7401     let order_id = create_ready_order(&sandbox, "freshness_missing_listing");
   7402     remove_orderable_listing(&sandbox, LISTING_ADDR);
   7403 
   7404     let (output, value) = sandbox.json_output(&[
   7405         "--format",
   7406         "json",
   7407         "--relay",
   7408         "ws://127.0.0.1:9",
   7409         "--approval-token",
   7410         "approve",
   7411         "order",
   7412         "submit",
   7413         order_id.as_str(),
   7414     ]);
   7415 
   7416     assert!(!output.status.success());
   7417     assert_eq!(output.status.code(), Some(3));
   7418     assert_eq!(value["operation_id"], "order.submit");
   7419     assert_eq!(value["errors"][0]["code"], "operation_unavailable");
   7420     assert_eq!(
   7421         value["errors"][0]["detail"]["issues"][0]["field"],
   7422         "order.listing_addr"
   7423     );
   7424     assert!(
   7425         value["errors"][0]["message"]
   7426             .as_str()
   7427             .expect("message")
   7428             .contains("listing is not active")
   7429     );
   7430 }
   7431 
   7432 #[test]
   7433 fn order_submit_rejects_superseded_local_listing_event_before_publish() {
   7434     let sandbox = RadrootsCliSandbox::new();
   7435     let order_id = create_ready_order(&sandbox, "freshness_superseded_listing");
   7436     let replacement_event_id = "3".repeat(64);
   7437     replace_latest_listing_event_id(&sandbox, LISTING_ADDR, replacement_event_id.as_str());
   7438 
   7439     let (output, value) = sandbox.json_output(&[
   7440         "--format",
   7441         "json",
   7442         "--relay",
   7443         "ws://127.0.0.1:9",
   7444         "--approval-token",
   7445         "approve",
   7446         "order",
   7447         "submit",
   7448         order_id.as_str(),
   7449     ]);
   7450 
   7451     assert!(!output.status.success());
   7452     assert_eq!(output.status.code(), Some(3));
   7453     assert_eq!(value["operation_id"], "order.submit");
   7454     assert_eq!(value["errors"][0]["code"], "operation_unavailable");
   7455     assert_eq!(
   7456         value["errors"][0]["detail"]["issues"][0]["field"],
   7457         "order.listing_event_id"
   7458     );
   7459     assert!(
   7460         value["errors"][0]["detail"]["issues"][0]["message"]
   7461             .as_str()
   7462             .expect("issue message")
   7463             .contains(replacement_event_id.as_str())
   7464     );
   7465 }
   7466 
   7467 #[test]
   7468 fn order_submit_rejects_over_available_quantity_before_publish() {
   7469     let sandbox = RadrootsCliSandbox::new();
   7470     sandbox.json_success(&["--format", "json", "account", "create"]);
   7471     seed_orderable_listing(&sandbox, LISTING_ADDR);
   7472     sandbox.json_success(&["--format", "json", "basket", "create", "over_quantity"]);
   7473     sandbox.json_success(&[
   7474         "--format",
   7475         "json",
   7476         "basket",
   7477         "item",
   7478         "add",
   7479         "over_quantity",
   7480         "--listing-addr",
   7481         LISTING_ADDR,
   7482         "--bin-id",
   7483         "bin-1",
   7484         "--quantity",
   7485         "6",
   7486     ]);
   7487     let quote = sandbox.json_success(&[
   7488         "--format",
   7489         "json",
   7490         "basket",
   7491         "quote",
   7492         "create",
   7493         "over_quantity",
   7494     ]);
   7495     let order_id = quote["result"]["quote"]["order_id"]
   7496         .as_str()
   7497         .expect("order id");
   7498 
   7499     let (output, value) = sandbox.json_output(&[
   7500         "--format",
   7501         "json",
   7502         "--relay",
   7503         "ws://127.0.0.1:9",
   7504         "--approval-token",
   7505         "approve",
   7506         "order",
   7507         "submit",
   7508         order_id,
   7509     ]);
   7510 
   7511     assert!(!output.status.success());
   7512     assert_eq!(output.status.code(), Some(10));
   7513     assert_eq!(value["operation_id"], "order.submit");
   7514     assert_eq!(value["errors"][0]["code"], "validation_failed");
   7515     assert_eq!(
   7516         value["errors"][0]["detail"]["issues"][0]["code"],
   7517         "order_quantity_exceeds_available"
   7518     );
   7519     assert!(
   7520         value["errors"][0]["detail"]["issues"][0]["message"]
   7521             .as_str()
   7522             .expect("issue message")
   7523             .contains("available quantity 5")
   7524     );
   7525     assert_no_removed_command_reference(&value, &["order", "submit"]);
   7526     assert_no_daemon_runtime_reference(&value, &["order", "submit"]);
   7527 }
   7528 
   7529 #[test]
   7530 fn order_submit_rejects_unknown_local_listing_bin_before_publish() {
   7531     let sandbox = RadrootsCliSandbox::new();
   7532     let order_id = create_ready_order(&sandbox, "unknown_bin");
   7533     rewrite_order_bin(&sandbox, order_id.as_str(), "unknown-bin");
   7534 
   7535     let (output, value) = sandbox.json_output(&[
   7536         "--format",
   7537         "json",
   7538         "--relay",
   7539         "ws://127.0.0.1:9",
   7540         "--approval-token",
   7541         "approve",
   7542         "order",
   7543         "submit",
   7544         order_id.as_str(),
   7545     ]);
   7546 
   7547     assert!(!output.status.success());
   7548     assert_eq!(output.status.code(), Some(10));
   7549     assert_eq!(value["operation_id"], "order.submit");
   7550     assert_eq!(value["errors"][0]["code"], "validation_failed");
   7551     assert_eq!(
   7552         value["errors"][0]["detail"]["issues"][0]["code"],
   7553         "order_bin_unknown"
   7554     );
   7555     assert_eq!(
   7556         value["errors"][0]["detail"]["issues"][0]["field"],
   7557         "order.items[0].bin_id"
   7558     );
   7559     assert!(
   7560         value["errors"][0]["detail"]["issues"][0]["message"]
   7561             .as_str()
   7562             .expect("issue message")
   7563             .contains("expected primary bin `bin-1`")
   7564     );
   7565     assert_no_removed_command_reference(&value, &["order", "submit"]);
   7566     assert_no_daemon_runtime_reference(&value, &["order", "submit"]);
   7567 }
   7568 
   7569 #[test]
   7570 fn basket_quote_rejects_missing_replica_before_order_write() {
   7571     let sandbox = RadrootsCliSandbox::new();
   7572     sandbox.json_success(&["--format", "json", "account", "create"]);
   7573     sandbox.json_success(&["--format", "json", "basket", "create", "missing_replica"]);
   7574     let add = sandbox.json_success(&[
   7575         "--format",
   7576         "json",
   7577         "basket",
   7578         "item",
   7579         "add",
   7580         "missing_replica",
   7581         "--listing-addr",
   7582         LISTING_ADDR,
   7583         "--bin-id",
   7584         "bin-1",
   7585         "--quantity",
   7586         "2",
   7587     ]);
   7588     assert_eq!(add["result"]["ready_for_quote"], false);
   7589     assert_eq!(
   7590         add["result"]["issues"][0]["code"],
   7591         "basket_market_replica_missing"
   7592     );
   7593 
   7594     let quote = sandbox.json_success(&[
   7595         "--format",
   7596         "json",
   7597         "basket",
   7598         "quote",
   7599         "create",
   7600         "missing_replica",
   7601     ]);
   7602     assert_eq!(quote["result"]["state"], "unconfigured");
   7603     assert_eq!(quote["result"]["ready_for_quote"], false);
   7604     assert_eq!(
   7605         quote["result"]["issues"][0]["code"],
   7606         "basket_market_replica_missing"
   7607     );
   7608     assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
   7609 }
   7610 
   7611 #[test]
   7612 fn basket_quote_rejects_unresolved_listing_before_order_write() {
   7613     let sandbox = RadrootsCliSandbox::new();
   7614     sandbox.json_success(&["--format", "json", "account", "create"]);
   7615     sandbox.json_success(&["--format", "json", "store", "init"]);
   7616     sandbox.json_success(&["--format", "json", "basket", "create", "unresolved_listing"]);
   7617     let add = sandbox.json_success(&[
   7618         "--format",
   7619         "json",
   7620         "basket",
   7621         "item",
   7622         "add",
   7623         "unresolved_listing",
   7624         "--listing-addr",
   7625         LISTING_ADDR,
   7626         "--bin-id",
   7627         "bin-1",
   7628         "--quantity",
   7629         "2",
   7630     ]);
   7631     assert_eq!(add["result"]["ready_for_quote"], false);
   7632     assert_eq!(
   7633         add["result"]["issues"][0]["code"],
   7634         "basket_item_listing_unresolved"
   7635     );
   7636 
   7637     let quote = sandbox.json_success(&[
   7638         "--format",
   7639         "json",
   7640         "basket",
   7641         "quote",
   7642         "create",
   7643         "unresolved_listing",
   7644     ]);
   7645     assert_eq!(quote["result"]["state"], "unconfigured");
   7646     assert_eq!(quote["result"]["ready_for_quote"], false);
   7647     assert_eq!(
   7648         quote["result"]["issues"][0]["code"],
   7649         "basket_item_listing_unresolved"
   7650     );
   7651     assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
   7652 }
   7653 
   7654 #[test]
   7655 fn basket_quote_rejects_ambiguous_listing_before_order_write() {
   7656     let sandbox = RadrootsCliSandbox::new();
   7657     sandbox.json_success(&["--format", "json", "account", "create"]);
   7658     seed_orderable_listing(&sandbox, LISTING_ADDR);
   7659     duplicate_orderable_listing_row(&sandbox, LISTING_ADDR);
   7660     sandbox.json_success(&["--format", "json", "basket", "create", "ambiguous_listing"]);
   7661     let add = sandbox.json_success(&[
   7662         "--format",
   7663         "json",
   7664         "basket",
   7665         "item",
   7666         "add",
   7667         "ambiguous_listing",
   7668         "--listing-addr",
   7669         LISTING_ADDR,
   7670         "--bin-id",
   7671         "bin-1",
   7672         "--quantity",
   7673         "2",
   7674     ]);
   7675     assert_eq!(add["result"]["ready_for_quote"], false);
   7676     assert_eq!(
   7677         add["result"]["issues"][0]["code"],
   7678         "basket_item_listing_ambiguous"
   7679     );
   7680 
   7681     let quote = sandbox.json_success(&[
   7682         "--format",
   7683         "json",
   7684         "basket",
   7685         "quote",
   7686         "create",
   7687         "ambiguous_listing",
   7688     ]);
   7689     assert_eq!(quote["result"]["state"], "unconfigured");
   7690     assert_eq!(quote["result"]["ready_for_quote"], false);
   7691     assert_eq!(
   7692         quote["result"]["issues"][0]["code"],
   7693         "basket_item_listing_ambiguous"
   7694     );
   7695     assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
   7696 }
   7697 
   7698 #[test]
   7699 fn basket_quote_rejects_invalid_verified_primary_bin_before_order_write() {
   7700     let sandbox = RadrootsCliSandbox::new();
   7701     sandbox.json_success(&["--format", "json", "account", "create"]);
   7702     seed_orderable_listing(&sandbox, LISTING_ADDR);
   7703     update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("missing-bin"));
   7704     sandbox.json_success(&[
   7705         "--format",
   7706         "json",
   7707         "basket",
   7708         "create",
   7709         "invalid_primary_bin",
   7710     ]);
   7711     let add = sandbox.json_success(&[
   7712         "--format",
   7713         "json",
   7714         "basket",
   7715         "item",
   7716         "add",
   7717         "invalid_primary_bin",
   7718         "--listing-addr",
   7719         LISTING_ADDR,
   7720         "--bin-id",
   7721         "bin-1",
   7722         "--quantity",
   7723         "2",
   7724     ]);
   7725     assert_eq!(add["result"]["ready_for_quote"], false);
   7726     assert_eq!(
   7727         add["result"]["issues"][0]["code"],
   7728         "listing_primary_bin_invalid"
   7729     );
   7730 
   7731     let validate = sandbox.json_success(&[
   7732         "--format",
   7733         "json",
   7734         "basket",
   7735         "validate",
   7736         "invalid_primary_bin",
   7737     ]);
   7738     assert_eq!(validate["result"]["state"], "unconfigured");
   7739     assert_eq!(validate["result"]["ready_for_quote"], false);
   7740     assert_eq!(
   7741         validate["result"]["issues"][0]["code"],
   7742         "listing_primary_bin_invalid"
   7743     );
   7744 
   7745     let quote = sandbox.json_success(&[
   7746         "--format",
   7747         "json",
   7748         "basket",
   7749         "quote",
   7750         "create",
   7751         "invalid_primary_bin",
   7752     ]);
   7753     assert_eq!(quote["result"]["state"], "unconfigured");
   7754     assert_eq!(quote["result"]["ready_for_quote"], false);
   7755     assert_eq!(
   7756         quote["result"]["issues"][0]["code"],
   7757         "listing_primary_bin_invalid"
   7758     );
   7759     assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
   7760 }
   7761 
   7762 #[test]
   7763 fn order_submit_rejects_stale_invalid_verified_primary_bin_before_relay_preflight() {
   7764     let sandbox = RadrootsCliSandbox::new();
   7765     let order_id = create_ready_order(&sandbox, "stale_invalid_bin");
   7766     update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("missing-bin"));
   7767 
   7768     let (output, value) = sandbox.json_output(&[
   7769         "--format",
   7770         "json",
   7771         "--dry-run",
   7772         "order",
   7773         "submit",
   7774         &order_id,
   7775     ]);
   7776 
   7777     assert!(!output.status.success());
   7778     assert_eq!(output.status.code(), Some(10));
   7779     assert_eq!(value["operation_id"], "order.submit");
   7780     assert_eq!(value["dry_run"], true);
   7781     assert_eq!(value["errors"][0]["code"], "validation_failed");
   7782     assert_eq!(
   7783         value["errors"][0]["detail"]["issues"][0]["code"],
   7784         "listing_primary_bin_invalid"
   7785     );
   7786     assert_eq!(
   7787         value["errors"][0]["detail"]["issues"][0]["field"],
   7788         "inventory.primary_bin_id"
   7789     );
   7790     assert_no_removed_command_reference(&value, &["order", "submit", "--dry-run"]);
   7791     assert_no_daemon_runtime_reference(&value, &["order", "submit", "--dry-run"]);
   7792 }
   7793 
   7794 #[test]
   7795 fn order_submit_dry_run_rejects_over_available_quantity_before_relay_preflight() {
   7796     let sandbox = RadrootsCliSandbox::new();
   7797     sandbox.json_success(&["--format", "json", "account", "create"]);
   7798     seed_orderable_listing(&sandbox, LISTING_ADDR);
   7799     sandbox.json_success(&["--format", "json", "basket", "create", "dry_over_quantity"]);
   7800     sandbox.json_success(&[
   7801         "--format",
   7802         "json",
   7803         "basket",
   7804         "item",
   7805         "add",
   7806         "dry_over_quantity",
   7807         "--listing-addr",
   7808         LISTING_ADDR,
   7809         "--bin-id",
   7810         "bin-1",
   7811         "--quantity",
   7812         "6",
   7813     ]);
   7814     let quote = sandbox.json_success(&[
   7815         "--format",
   7816         "json",
   7817         "basket",
   7818         "quote",
   7819         "create",
   7820         "dry_over_quantity",
   7821     ]);
   7822     let order_id = quote["result"]["quote"]["order_id"]
   7823         .as_str()
   7824         .expect("order id");
   7825 
   7826     let (output, value) =
   7827         sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   7828 
   7829     assert!(!output.status.success());
   7830     assert_eq!(output.status.code(), Some(10));
   7831     assert_eq!(value["operation_id"], "order.submit");
   7832     assert_eq!(value["dry_run"], true);
   7833     assert_eq!(value["errors"][0]["code"], "validation_failed");
   7834     assert_eq!(
   7835         value["errors"][0]["detail"]["issues"][0]["code"],
   7836         "order_quantity_exceeds_available"
   7837     );
   7838 }
   7839 
   7840 #[test]
   7841 fn ready_order_submit_dry_run_validates_local_buyer_authority() {
   7842     let sandbox = RadrootsCliSandbox::new();
   7843     let first = sandbox.json_success(&["--format", "json", "account", "create"]);
   7844     let first_account_id = first["result"]["account"]["id"]
   7845         .as_str()
   7846         .expect("first account id");
   7847     let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
   7848     sandbox.json_success(&["--format", "json", "basket", "create", "ready_order"]);
   7849     sandbox.json_success(&[
   7850         "--format",
   7851         "json",
   7852         "basket",
   7853         "item",
   7854         "add",
   7855         "ready_order",
   7856         "--listing-addr",
   7857         LISTING_ADDR,
   7858         "--bin-id",
   7859         "bin-1",
   7860         "--quantity",
   7861         "2",
   7862     ]);
   7863     let quote = sandbox.json_success(&[
   7864         "--format",
   7865         "json",
   7866         "basket",
   7867         "quote",
   7868         "create",
   7869         "ready_order",
   7870     ]);
   7871     let order_id = quote["result"]["quote"]["order_id"]
   7872         .as_str()
   7873         .expect("order id");
   7874     assert_eq!(quote["result"]["quote"]["ready_for_submit"], true);
   7875     assert_eq!(
   7876         quote["result"]["order"]["buyer_account_id"],
   7877         first_account_id
   7878     );
   7879     assert_eq!(
   7880         quote["result"]["order"]["listing_event_id"],
   7881         listing_event_id
   7882     );
   7883 
   7884     let dry_run =
   7885         sandbox.json_success(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   7886     assert_eq!(dry_run["operation_id"], "order.submit");
   7887     assert_eq!(dry_run["dry_run"], true);
   7888     assert_eq!(dry_run["result"]["state"], "dry_run");
   7889     assert_eq!(dry_run["result"]["source"], "SDK order submit ยท local key");
   7890     assert_eq!(dry_run["result"]["event_kind"], 3422);
   7891     assert_eq!(
   7892         dry_run["result"]["target_relays"][0],
   7893         ORDERABLE_LISTING_RELAY
   7894     );
   7895     assert_no_daemon_runtime_reference(&dry_run, &["order", "submit", "--dry-run"]);
   7896 
   7897     let second = sandbox.json_success(&["--format", "json", "account", "create"]);
   7898     let second_account_id = second["result"]["account"]["id"]
   7899         .as_str()
   7900         .expect("second account id");
   7901     sandbox.json_success(&[
   7902         "--format",
   7903         "json",
   7904         "account",
   7905         "selection",
   7906         "update",
   7907         second_account_id,
   7908     ]);
   7909     let drift =
   7910         sandbox.json_success(&["--format", "json", "--dry-run", "order", "submit", order_id]);
   7911     assert_eq!(drift["operation_id"], "order.submit");
   7912     assert_eq!(drift["result"]["state"], "dry_run");
   7913     assert_eq!(drift["result"]["buyer_account_id"], first_account_id);
   7914 
   7915     let (output, mismatch) = sandbox.json_output(&[
   7916         "--format",
   7917         "json",
   7918         "--account-id",
   7919         second_account_id,
   7920         "--dry-run",
   7921         "order",
   7922         "submit",
   7923         order_id,
   7924     ]);
   7925 
   7926     assert!(!output.status.success());
   7927     assert_eq!(output.status.code(), Some(5));
   7928     assert_eq!(mismatch["operation_id"], "order.submit");
   7929     assert_eq!(mismatch["errors"][0]["code"], "account_mismatch");
   7930     assert_eq!(mismatch["errors"][0]["detail"]["class"], "account");
   7931     assert_no_removed_command_reference(&mismatch, &["order", "submit", "--dry-run"]);
   7932     assert_no_daemon_runtime_reference(&mismatch, &["order", "submit", "--dry-run"]);
   7933 
   7934     let (network_output, network_mismatch) = sandbox.json_output(&[
   7935         "--format",
   7936         "json",
   7937         "--account-id",
   7938         second_account_id,
   7939         "--relay",
   7940         "ws://127.0.0.1:9",
   7941         "--approval-token",
   7942         "approve",
   7943         "order",
   7944         "submit",
   7945         order_id,
   7946     ]);
   7947 
   7948     assert!(!network_output.status.success());
   7949     assert_eq!(network_output.status.code(), Some(5));
   7950     assert_eq!(network_mismatch["operation_id"], "order.submit");
   7951     assert_eq!(network_mismatch["result"], Value::Null);
   7952     assert_eq!(network_mismatch["errors"][0]["code"], "account_mismatch");
   7953     assert_eq!(network_mismatch["errors"][0]["detail"]["class"], "account");
   7954     assert_no_daemon_runtime_reference(&network_mismatch, &["order", "submit"]);
   7955 }
   7956 
   7957 #[test]
   7958 fn seller_target_flow_acceptance_uses_target_operations() {
   7959     let sandbox = RadrootsCliSandbox::new();
   7960 
   7961     let account = sandbox.json_success(&["--format", "json", "account", "create"]);
   7962     let account_id = account["result"]["account"]["id"]
   7963         .as_str()
   7964         .expect("account id");
   7965     assert_eq!(account["operation_id"], "account.create");
   7966     assert_eq!(account["result"]["account"]["signer"], "local");
   7967     assert_eq!(account["result"]["account"]["custody"], "secret_backed");
   7968     assert_eq!(account["result"]["account"]["write_capable"], true);
   7969     assert_no_removed_command_reference(&account, &["account", "create"]);
   7970 
   7971     let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
   7972     assert_eq!(signer["operation_id"], "signer.status.get");
   7973     assert_eq!(signer["result"]["mode"], "local");
   7974     assert_eq!(signer["result"]["state"], "ready");
   7975     assert_eq!(signer["result"]["signer_account_id"], account_id);
   7976     assert_no_removed_command_reference(&signer, &["signer", "status", "get"]);
   7977 
   7978     let farm = sandbox.json_success(&[
   7979         "--format",
   7980         "json",
   7981         "farm",
   7982         "create",
   7983         "--name",
   7984         "Green Farm",
   7985         "--location",
   7986         "farmstand",
   7987         "--country",
   7988         "US",
   7989         "--delivery-method",
   7990         "pickup",
   7991     ]);
   7992     assert_eq!(farm["operation_id"], "farm.create");
   7993     assert_eq!(farm["result"]["state"], "saved");
   7994     assert_no_removed_command_reference(&farm, &["farm", "create"]);
   7995 
   7996     let create = sandbox.json_success(&[
   7997         "--format",
   7998         "json",
   7999         "listing",
   8000         "create",
   8001         "--key",
   8002         "eggs",
   8003         "--title",
   8004         "Eggs",
   8005         "--category",
   8006         "eggs",
   8007         "--summary",
   8008         "Fresh eggs",
   8009         "--bin-id",
   8010         "bin-1",
   8011         "--quantity-amount",
   8012         "1",
   8013         "--quantity-unit",
   8014         "each",
   8015         "--price-amount",
   8016         "6",
   8017         "--price-currency",
   8018         "USD",
   8019         "--price-per-amount",
   8020         "1",
   8021         "--price-per-unit",
   8022         "each",
   8023         "--available",
   8024         "10",
   8025     ]);
   8026     let listing_file = create["result"]["file"].as_str().expect("listing file");
   8027     assert_eq!(create["operation_id"], "listing.create");
   8028     assert!(Path::new(listing_file).exists());
   8029     assert_no_removed_command_reference(&create, &["listing", "create"]);
   8030 
   8031     let list = sandbox.json_success(&["--format", "json", "listing", "list"]);
   8032     assert_eq!(list["operation_id"], "listing.list");
   8033     assert_eq!(list["result"]["state"], "ready");
   8034     assert_eq!(list["result"]["count"], 1);
   8035     assert_eq!(
   8036         list["result"]["listings"][0]["id"],
   8037         create["result"]["listing_id"]
   8038     );
   8039     assert_eq!(list["result"]["listings"][0]["state"], "ready");
   8040     assert_no_removed_command_reference(&list, &["listing", "list"]);
   8041 
   8042     let validate = sandbox.json_success(&["--format", "json", "listing", "validate", listing_file]);
   8043     assert_eq!(validate["operation_id"], "listing.validate");
   8044     assert_eq!(validate["result"]["valid"], true);
   8045     assert_eq!(validate["result"]["issues"], Value::Null);
   8046     assert_no_removed_command_reference(&validate, &["listing", "validate"]);
   8047 
   8048     let publish = sandbox.json_success(&[
   8049         "--format",
   8050         "json",
   8051         "--dry-run",
   8052         "listing",
   8053         "publish",
   8054         listing_file,
   8055     ]);
   8056     assert_eq!(publish["operation_id"], "listing.publish");
   8057     assert_eq!(publish["result"]["state"], "dry_run");
   8058     assert_no_removed_command_reference(&publish, &["listing", "publish", "--dry-run"]);
   8059     assert_no_daemon_runtime_reference(&publish, &["listing", "publish", "--dry-run"]);
   8060 
   8061     let archive = sandbox.json_success(&[
   8062         "--format",
   8063         "json",
   8064         "--dry-run",
   8065         "listing",
   8066         "archive",
   8067         listing_file,
   8068     ]);
   8069     assert_eq!(archive["operation_id"], "listing.archive");
   8070     assert_eq!(archive["result"]["state"], "dry_run");
   8071     assert_eq!(archive["result"]["operation"], "archive");
   8072     assert_no_removed_command_reference(&archive, &["listing", "archive", "--dry-run"]);
   8073     assert_no_daemon_runtime_reference(&archive, &["listing", "archive", "--dry-run"]);
   8074 
   8075     let (publish_output, unavailable_publish) = sandbox.json_output(&[
   8076         "--format",
   8077         "json",
   8078         "--approval-token",
   8079         "approve",
   8080         "listing",
   8081         "publish",
   8082         listing_file,
   8083     ]);
   8084     assert!(!publish_output.status.success());
   8085     assert_eq!(unavailable_publish["operation_id"], "listing.publish");
   8086     assert_eq!(
   8087         unavailable_publish["errors"][0]["code"],
   8088         "empty_target_relays"
   8089     );
   8090     assert_eq!(
   8091         unavailable_publish["errors"][0]["detail"]["class"],
   8092         "configuration"
   8093     );
   8094     assert_no_removed_command_reference(&unavailable_publish, &["listing", "publish"]);
   8095     assert_no_daemon_runtime_reference(&unavailable_publish, &["listing", "publish"]);
   8096 
   8097     let (archive_output, unavailable_archive) = sandbox.json_output(&[
   8098         "--format",
   8099         "json",
   8100         "--approval-token",
   8101         "approve",
   8102         "listing",
   8103         "archive",
   8104         listing_file,
   8105     ]);
   8106     assert!(!archive_output.status.success());
   8107     assert_eq!(unavailable_archive["operation_id"], "listing.archive");
   8108     assert_eq!(
   8109         unavailable_archive["errors"][0]["code"],
   8110         "empty_target_relays"
   8111     );
   8112     assert_eq!(
   8113         unavailable_archive["errors"][0]["detail"]["class"],
   8114         "configuration"
   8115     );
   8116     assert_no_removed_command_reference(&unavailable_archive, &["listing", "archive"]);
   8117     assert_no_daemon_runtime_reference(&unavailable_archive, &["listing", "archive"]);
   8118 }