cli

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

market.rs (26628B)


      1 use serde::Serialize;
      2 use serde_json::Value;
      3 
      4 use crate::cli::global::{FindQueryArgs, RecordLookupArgs};
      5 use crate::ops::{
      6     MarketListingGetRequest, MarketListingGetResult, MarketProductSearchRequest,
      7     MarketProductSearchResult, MarketRefreshRequest, MarketRefreshResult, OperationAdapterError,
      8     OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult,
      9     OperationResultData, OperationService,
     10 };
     11 use crate::runtime::RuntimeError;
     12 use crate::runtime::config::RuntimeConfig;
     13 use crate::view::runtime::{FindView, ListingGetView, SyncActionView};
     14 
     15 pub struct MarketOperationService<'a> {
     16     config: &'a RuntimeConfig,
     17 }
     18 
     19 impl<'a> MarketOperationService<'a> {
     20     pub fn new(config: &'a RuntimeConfig) -> Self {
     21         Self { config }
     22     }
     23 }
     24 
     25 impl OperationService<MarketRefreshRequest> for MarketOperationService<'_> {
     26     type Result = MarketRefreshResult;
     27 
     28     fn execute(
     29         &self,
     30         _request: OperationRequest<MarketRefreshRequest>,
     31     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     32         let view = market_refresh_view(map_runtime(crate::runtime::sync::market_refresh(
     33             self.config,
     34         ))?);
     35         serialized_operation_result::<MarketRefreshResult, _>(&view)
     36     }
     37 }
     38 
     39 impl OperationService<MarketProductSearchRequest> for MarketOperationService<'_> {
     40     type Result = MarketProductSearchResult;
     41 
     42     fn execute(
     43         &self,
     44         request: OperationRequest<MarketProductSearchRequest>,
     45     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     46         let args = FindQueryArgs {
     47             query: required_query_terms(&request)?,
     48         };
     49         let view = market_product_search_view(map_runtime(crate::runtime::find::search(
     50             self.config,
     51             &args,
     52         ))?);
     53         serialized_operation_result::<MarketProductSearchResult, _>(&view)
     54     }
     55 }
     56 
     57 impl OperationService<MarketListingGetRequest> for MarketOperationService<'_> {
     58     type Result = MarketListingGetResult;
     59 
     60     fn execute(
     61         &self,
     62         request: OperationRequest<MarketListingGetRequest>,
     63     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     64         let args = RecordLookupArgs {
     65             key: required_lookup(&request)?,
     66         };
     67         let view = market_listing_get_view(map_runtime(crate::runtime::listing::get(
     68             self.config,
     69             &args,
     70         ))?);
     71         serialized_operation_result::<MarketListingGetResult, _>(&view)
     72     }
     73 }
     74 
     75 fn market_refresh_view(mut view: SyncActionView) -> SyncActionView {
     76     view.actions = match view.state.as_str() {
     77         "ready" => vec!["radroots market product search tomatoes".to_owned()],
     78         "unavailable" => vec!["radroots sync status get".to_owned()],
     79         "unconfigured" => {
     80             let mut actions = Vec::new();
     81             if view.replica_db == "missing" {
     82                 actions.push("radroots store init".to_owned());
     83             }
     84             if view.relay_count == 0 {
     85                 actions.push("radroots --relay wss://relay.example.com market refresh".to_owned());
     86             }
     87             if actions.is_empty() {
     88                 actions.extend(std::mem::take(&mut view.actions));
     89             }
     90             actions
     91         }
     92         _ => std::mem::take(&mut view.actions),
     93     };
     94     view
     95 }
     96 
     97 fn market_product_search_view(mut view: FindView) -> FindView {
     98     view.actions = match view.state.as_str() {
     99         "ready" => view
    100             .results
    101             .first()
    102             .map(|result| {
    103                 let mut actions = vec![format!(
    104                     "radroots market listing get {}",
    105                     result.product_key
    106                 )];
    107                 if result.readiness.order_request_enabled {
    108                     actions.push("radroots basket create".to_owned());
    109                 }
    110                 actions
    111             })
    112             .unwrap_or_default(),
    113         "empty" => vec![
    114             "radroots market refresh".to_owned(),
    115             "radroots market product search eggs".to_owned(),
    116         ],
    117         "unconfigured" => vec![
    118             "radroots store init".to_owned(),
    119             "radroots market refresh".to_owned(),
    120         ],
    121         _ => std::mem::take(&mut view.actions),
    122     };
    123     view
    124 }
    125 
    126 fn market_listing_get_view(mut view: ListingGetView) -> ListingGetView {
    127     view.actions = match view.state.as_str() {
    128         "ready" => {
    129             if view.readiness.order_request_enabled {
    130                 vec!["radroots basket create".to_owned()]
    131             } else {
    132                 Vec::new()
    133             }
    134         }
    135         "missing" => vec![
    136             "radroots market product search tomatoes".to_owned(),
    137             "radroots market refresh".to_owned(),
    138         ],
    139         "unconfigured" => vec![
    140             "radroots store init".to_owned(),
    141             "radroots market refresh".to_owned(),
    142         ],
    143         _ => std::mem::take(&mut view.actions),
    144     };
    145     view
    146 }
    147 
    148 fn required_query_terms<P>(
    149     request: &OperationRequest<P>,
    150 ) -> Result<Vec<String>, OperationAdapterError>
    151 where
    152     P: OperationRequestPayload + OperationRequestData,
    153 {
    154     let input = request.payload.input();
    155     let Some(value) = input.get("query").or_else(|| input.get("terms")) else {
    156         return Err(invalid_input(
    157             request.operation_id(),
    158             "missing required `query` input".to_owned(),
    159         ));
    160     };
    161     let terms = match value {
    162         Value::String(value) => value
    163             .split_whitespace()
    164             .map(str::trim)
    165             .filter(|term| !term.is_empty())
    166             .map(str::to_owned)
    167             .collect::<Vec<_>>(),
    168         Value::Array(values) => values
    169             .iter()
    170             .map(|value| {
    171                 value.as_str().map(str::to_owned).ok_or_else(|| {
    172                     invalid_input(
    173                         request.operation_id(),
    174                         "`query` array entries must be strings".to_owned(),
    175                     )
    176                 })
    177             })
    178             .collect::<Result<Vec<_>, _>>()?,
    179         _ => {
    180             return Err(invalid_input(
    181                 request.operation_id(),
    182                 "`query` input must be a string or string array".to_owned(),
    183             ));
    184         }
    185     };
    186 
    187     if terms.is_empty() {
    188         return Err(invalid_input(
    189             request.operation_id(),
    190             "`query` input must not be empty".to_owned(),
    191         ));
    192     }
    193     Ok(terms)
    194 }
    195 
    196 fn required_lookup<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError>
    197 where
    198     P: OperationRequestPayload + OperationRequestData,
    199 {
    200     string_input(request, "key")
    201         .or_else(|| string_input(request, "listing_id"))
    202         .or_else(|| string_input(request, "listing"))
    203         .ok_or_else(|| {
    204             invalid_input(
    205                 request.operation_id(),
    206                 "missing required `key` input".to_owned(),
    207             )
    208         })
    209 }
    210 
    211 fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
    212 where
    213     R: OperationResultData,
    214     T: Serialize,
    215 {
    216     OperationResult::new(R::from_serializable(value)?)
    217 }
    218 
    219 fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> {
    220     result.map_err(|error| OperationAdapterError::Runtime(error.to_string()))
    221 }
    222 
    223 fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
    224 where
    225     P: OperationRequestPayload + OperationRequestData,
    226 {
    227     request
    228         .payload
    229         .input()
    230         .get(key)
    231         .and_then(Value::as_str)
    232         .map(str::to_owned)
    233 }
    234 
    235 fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
    236     OperationAdapterError::InvalidInput {
    237         operation_id: operation_id.to_owned(),
    238         message,
    239     }
    240 }
    241 
    242 #[cfg(test)]
    243 mod tests {
    244     use std::path::{Path, PathBuf};
    245 
    246     use radroots_runtime_paths::RadrootsMigrationReport;
    247     use radroots_secret_vault::RadrootsSecretBackend;
    248     use serde_json::{Map, Value};
    249     use tempfile::tempdir;
    250 
    251     use super::{MarketOperationService, market_listing_get_view, market_product_search_view};
    252     use crate::ops::{
    253         MarketListingGetRequest, MarketProductSearchRequest, MarketRefreshRequest,
    254         OperationAdapter, OperationContext, OperationData, OperationRequest,
    255     };
    256     use crate::runtime::config::{
    257         AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
    258         LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
    259         PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
    260         RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
    261         SignerConfig, Verbosity,
    262     };
    263     use crate::view::runtime::{
    264         FindPriceView, FindQuantityView, FindResultProvenanceView, FindResultView, FindView,
    265         ListingGetView, MarketReadinessView, SyncFreshnessView,
    266     };
    267 
    268     const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
    269 
    270     #[test]
    271     fn market_refresh_preserves_unconfigured_ingest_truth() {
    272         let dir = tempdir().expect("tempdir");
    273         let config = sample_config(dir.path());
    274         let service = OperationAdapter::new(MarketOperationService::new(&config));
    275         let request =
    276             OperationRequest::new(OperationContext::default(), MarketRefreshRequest::default())
    277                 .expect("market refresh request");
    278         let envelope = service
    279             .execute(request)
    280             .expect("market refresh result")
    281             .to_envelope(OperationContext::default().envelope_context("req_market_refresh"))
    282             .expect("market refresh envelope");
    283 
    284         assert_eq!(envelope.operation_id, "market.refresh");
    285         assert_eq!(envelope.result["state"], "unconfigured");
    286         assert_eq!(envelope.result["direction"], "pull");
    287         assert_eq!(envelope.result["actions"][0], "radroots store init");
    288     }
    289 
    290     #[test]
    291     fn market_refresh_supports_dry_run() {
    292         let dir = tempdir().expect("tempdir");
    293         let config = sample_config(dir.path());
    294         let service = OperationAdapter::new(MarketOperationService::new(&config));
    295         let mut context = OperationContext::default();
    296         context.dry_run = true;
    297         let request = OperationRequest::new(context.clone(), MarketRefreshRequest::default())
    298             .expect("market refresh request");
    299         let envelope = service
    300             .execute(request)
    301             .expect("market refresh dry run")
    302             .to_envelope(context.envelope_context("req_market_refresh"))
    303             .expect("market refresh envelope");
    304 
    305         assert_eq!(envelope.operation_id, "market.refresh");
    306         assert_eq!(envelope.dry_run, true);
    307         assert_eq!(envelope.result["state"], "unconfigured");
    308         assert_eq!(envelope.result["replica_db"], "missing");
    309         assert_eq!(envelope.result["direction"], "pull");
    310     }
    311 
    312     #[test]
    313     fn market_refresh_dry_run_skips_relay_fetch_when_store_is_ready() {
    314         let dir = tempdir().expect("tempdir");
    315         let mut config = sample_config(dir.path());
    316         config.output.dry_run = true;
    317         config.relay.urls = vec!["wss://relay.example.com".to_owned()];
    318         crate::runtime::store::init(&config).expect("store init");
    319 
    320         let service = OperationAdapter::new(MarketOperationService::new(&config));
    321         let mut context = OperationContext::default();
    322         context.dry_run = true;
    323         let request = OperationRequest::new(context.clone(), MarketRefreshRequest::default())
    324             .expect("market refresh request");
    325         let envelope = service
    326             .execute(request)
    327             .expect("market refresh dry run")
    328             .to_envelope(context.envelope_context("req_market_refresh"))
    329             .expect("market refresh envelope");
    330 
    331         assert_eq!(envelope.operation_id, "market.refresh");
    332         assert_eq!(envelope.result["state"], "ready");
    333         assert_eq!(
    334             envelope.result["target_relays"][0],
    335             "wss://relay.example.com"
    336         );
    337         assert_eq!(envelope.result["fetched_count"], 0);
    338         assert_eq!(envelope.result["ingested_count"], 0);
    339         assert_eq!(envelope.result["skipped_count"], 0);
    340         assert_eq!(envelope.result["unsupported_count"], 0);
    341         assert!(
    342             envelope.result["reason"]
    343                 .as_str()
    344                 .expect("reason")
    345                 .contains("dry run")
    346         );
    347     }
    348 
    349     #[test]
    350     fn market_refresh_no_relay_action_is_actionable() {
    351         let dir = tempdir().expect("tempdir");
    352         let config = sample_config(dir.path());
    353         crate::runtime::store::init(&config).expect("store init");
    354         let service = OperationAdapter::new(MarketOperationService::new(&config));
    355         let request =
    356             OperationRequest::new(OperationContext::default(), MarketRefreshRequest::default())
    357                 .expect("market refresh request");
    358         let envelope = service
    359             .execute(request)
    360             .expect("market refresh result")
    361             .to_envelope(OperationContext::default().envelope_context("req_market_refresh"))
    362             .expect("market refresh envelope");
    363 
    364         assert_eq!(envelope.result["state"], "unconfigured");
    365         assert_eq!(
    366             envelope.result["actions"][0],
    367             "radroots --relay wss://relay.example.com market refresh"
    368         );
    369     }
    370 
    371     #[test]
    372     fn market_product_search_uses_find_runtime_without_top_level_find() {
    373         let dir = tempdir().expect("tempdir");
    374         let config = sample_config(dir.path());
    375         let service = OperationAdapter::new(MarketOperationService::new(&config));
    376         let request = OperationRequest::new(
    377             OperationContext::default(),
    378             MarketProductSearchRequest::from_data(data(&[("query", "eggs")])),
    379         )
    380         .expect("market product search request");
    381         let envelope = service
    382             .execute(request)
    383             .expect("market product search result")
    384             .to_envelope(OperationContext::default().envelope_context("req_market_search"))
    385             .expect("market product search envelope");
    386 
    387         assert_eq!(envelope.operation_id, "market.product.search");
    388         assert_eq!(envelope.result["state"], "unconfigured");
    389         assert_eq!(envelope.result["query"], "eggs");
    390         assert_eq!(envelope.result["actions"][0], "radroots store init");
    391     }
    392 
    393     #[test]
    394     fn market_listing_get_requires_lookup_key() {
    395         let dir = tempdir().expect("tempdir");
    396         let config = sample_config(dir.path());
    397         let service = OperationAdapter::new(MarketOperationService::new(&config));
    398         let request = OperationRequest::new(
    399             OperationContext::default(),
    400             MarketListingGetRequest::default(),
    401         )
    402         .expect("market listing get request");
    403         let error = service.execute(request).expect_err("key required");
    404 
    405         assert!(format!("{error}").contains("`key`"));
    406     }
    407 
    408     #[test]
    409     fn market_listing_get_wraps_listing_runtime_with_target_actions() {
    410         let dir = tempdir().expect("tempdir");
    411         let config = sample_config(dir.path());
    412         let service = OperationAdapter::new(MarketOperationService::new(&config));
    413         let request = OperationRequest::new(
    414             OperationContext::default(),
    415             MarketListingGetRequest::from_data(data(&[("key", "eggs")])),
    416         )
    417         .expect("market listing get request");
    418         let envelope = service
    419             .execute(request)
    420             .expect("market listing get result")
    421             .to_envelope(OperationContext::default().envelope_context("req_market_listing"))
    422             .expect("market listing get envelope");
    423 
    424         assert_eq!(envelope.operation_id, "market.listing.get");
    425         assert_eq!(envelope.result["state"], "unconfigured");
    426         assert_eq!(envelope.result["actions"][0], "radroots store init");
    427     }
    428 
    429     #[test]
    430     fn market_ready_actions_do_not_emit_incomplete_basket_item_add_commands() {
    431         let search = market_product_search_view(FindView {
    432             state: "ready".to_owned(),
    433             source: "test".to_owned(),
    434             query: "eggs".to_owned(),
    435             count: 1,
    436             relay_count: 1,
    437             replica_db: "ready".to_owned(),
    438             freshness: freshness(),
    439             results: vec![FindResultView {
    440                 id: "listing_eggs".to_owned(),
    441                 product_key: "eggs".to_owned(),
    442                 readiness: readiness_enabled(),
    443                 listing_addr: Some(LISTING_ADDR.to_owned()),
    444                 primary_bin_id: Some("bin-1".to_owned()),
    445                 title: "Eggs".to_owned(),
    446                 category: "eggs".to_owned(),
    447                 summary: None,
    448                 location_primary: None,
    449                 available: quantity(),
    450                 price: price(),
    451                 provenance: provenance(),
    452                 hyf: None,
    453             }],
    454             hyf: None,
    455             reason: None,
    456             actions: Vec::new(),
    457         });
    458 
    459         assert_eq!(
    460             search.actions,
    461             vec![
    462                 "radroots market listing get eggs".to_owned(),
    463                 "radroots basket create".to_owned()
    464             ]
    465         );
    466 
    467         let listing = market_listing_get_view(ListingGetView {
    468             state: "ready".to_owned(),
    469             source: "test".to_owned(),
    470             lookup: "eggs".to_owned(),
    471             readiness: readiness_enabled(),
    472             listing_id: Some("listing_eggs".to_owned()),
    473             product_key: Some("eggs".to_owned()),
    474             listing_addr: Some(LISTING_ADDR.to_owned()),
    475             primary_bin_id: Some("bin-1".to_owned()),
    476             title: Some("Eggs".to_owned()),
    477             category: Some("eggs".to_owned()),
    478             description: None,
    479             location_primary: None,
    480             available: Some(quantity()),
    481             price: Some(price()),
    482             provenance: provenance(),
    483             reason: None,
    484             actions: Vec::new(),
    485         });
    486 
    487         assert_eq!(listing.actions, vec!["radroots basket create".to_owned()]);
    488         assert!(
    489             search
    490                 .actions
    491                 .iter()
    492                 .chain(listing.actions.iter())
    493                 .all(|action| !action.starts_with("radroots basket item add "))
    494         );
    495     }
    496 
    497     #[test]
    498     fn market_ready_actions_require_order_request_enabled() {
    499         let disabled_search = market_product_search_view(FindView {
    500             state: "ready".to_owned(),
    501             source: "test".to_owned(),
    502             query: "eggs".to_owned(),
    503             count: 1,
    504             relay_count: 1,
    505             replica_db: "ready".to_owned(),
    506             freshness: freshness(),
    507             results: vec![FindResultView {
    508                 id: "listing_eggs".to_owned(),
    509                 product_key: "eggs".to_owned(),
    510                 readiness: readiness_disabled(),
    511                 listing_addr: Some(LISTING_ADDR.to_owned()),
    512                 primary_bin_id: Some("bin-1".to_owned()),
    513                 title: "Eggs".to_owned(),
    514                 category: "eggs".to_owned(),
    515                 summary: None,
    516                 location_primary: None,
    517                 available: quantity(),
    518                 price: price(),
    519                 provenance: provenance(),
    520                 hyf: None,
    521             }],
    522             hyf: None,
    523             reason: None,
    524             actions: Vec::new(),
    525         });
    526 
    527         assert_eq!(
    528             disabled_search.actions,
    529             vec!["radroots market listing get eggs".to_owned()]
    530         );
    531 
    532         let disabled_listing = market_listing_get_view(ListingGetView {
    533             state: "ready".to_owned(),
    534             source: "test".to_owned(),
    535             lookup: "eggs".to_owned(),
    536             readiness: readiness_disabled(),
    537             listing_id: Some("listing_eggs".to_owned()),
    538             product_key: Some("eggs".to_owned()),
    539             listing_addr: Some(LISTING_ADDR.to_owned()),
    540             primary_bin_id: Some("bin-1".to_owned()),
    541             title: Some("Eggs".to_owned()),
    542             category: Some("eggs".to_owned()),
    543             description: None,
    544             location_primary: None,
    545             available: Some(quantity()),
    546             price: Some(price()),
    547             provenance: provenance(),
    548             reason: None,
    549             actions: Vec::new(),
    550         });
    551 
    552         assert!(disabled_listing.actions.is_empty());
    553     }
    554 
    555     fn freshness() -> SyncFreshnessView {
    556         SyncFreshnessView {
    557             state: "fresh".to_owned(),
    558             display: "fresh".to_owned(),
    559             age_seconds: Some(0),
    560             last_event_at: Some(0),
    561             run: None,
    562         }
    563     }
    564 
    565     fn provenance() -> FindResultProvenanceView {
    566         FindResultProvenanceView {
    567             origin: "fixture".to_owned(),
    568             freshness: "fresh".to_owned(),
    569             relay_count: 1,
    570         }
    571     }
    572 
    573     fn quantity() -> FindQuantityView {
    574         FindQuantityView {
    575             total_amount: 1.0,
    576             total_unit: "each".to_owned(),
    577             label: None,
    578             available_amount: Some(1),
    579         }
    580     }
    581 
    582     fn price() -> FindPriceView {
    583         FindPriceView {
    584             amount: 6.0,
    585             currency: "USD".to_owned(),
    586             per_amount: 1.0,
    587             per_unit: "each".to_owned(),
    588         }
    589     }
    590 
    591     fn readiness_enabled() -> MarketReadinessView {
    592         MarketReadinessView {
    593             protocol_valid: true,
    594             marketplace_eligible: true,
    595             order_request_enabled: true,
    596             primary_bin_verified: true,
    597             reason_codes: Vec::new(),
    598         }
    599     }
    600 
    601     fn readiness_disabled() -> MarketReadinessView {
    602         MarketReadinessView {
    603             protocol_valid: true,
    604             marketplace_eligible: true,
    605             order_request_enabled: false,
    606             primary_bin_verified: true,
    607             reason_codes: vec![
    608                 "listing_order_request_disabled".to_owned(),
    609                 "listing_inventory_unavailable".to_owned(),
    610             ],
    611         }
    612     }
    613 
    614     fn sample_config(root: &Path) -> RuntimeConfig {
    615         let data = root.join("data");
    616         let logs = root.join("logs");
    617         let secrets = root.join("secrets");
    618         RuntimeConfig {
    619             output: OutputConfig {
    620                 format: OutputFormat::Human,
    621                 verbosity: Verbosity::Normal,
    622                 color: true,
    623                 dry_run: false,
    624             },
    625             interaction: InteractionConfig {
    626                 input_enabled: true,
    627                 assume_yes: false,
    628                 stdin_tty: false,
    629                 stdout_tty: false,
    630                 prompts_allowed: false,
    631                 confirmations_allowed: false,
    632             },
    633             paths: PathsConfig {
    634                 profile: "interactive_user".into(),
    635                 profile_source: "test".into(),
    636                 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
    637                 root_source: "test".into(),
    638                 repo_local_root: None,
    639                 repo_local_root_source: None,
    640                 subordinate_path_override_source: "runtime_config".into(),
    641                 app_namespace: "apps/cli".into(),
    642                 shared_accounts_namespace: "shared/accounts".into(),
    643                 shared_identities_namespace: "shared/identities".into(),
    644                 app_config_path: root.join("config/apps/cli/config.toml"),
    645                 workspace_config_path: None,
    646                 app_data_root: data.join("apps/cli"),
    647                 app_logs_root: logs.join("apps/cli"),
    648                 shared_accounts_data_root: data.join("shared/accounts"),
    649                 shared_accounts_secrets_root: secrets.join("shared/accounts"),
    650                 default_identity_path: secrets.join("shared/identities/default.json"),
    651             },
    652             migration: MigrationConfig {
    653                 report: RadrootsMigrationReport::empty(),
    654             },
    655             logging: LoggingConfig {
    656                 filter: "info".into(),
    657                 directory: None,
    658                 stdout: false,
    659             },
    660             account: AccountConfig {
    661                 selector: None,
    662                 store_path: data.join("shared/accounts/store.json"),
    663                 secrets_dir: secrets.join("shared/accounts"),
    664                 secret_backend: RadrootsSecretBackend::EncryptedFile,
    665                 secret_fallback: None,
    666             },
    667             account_secret_contract: AccountSecretContractConfig {
    668                 default_backend: "host_vault".into(),
    669                 default_fallback: Some("encrypted_file".into()),
    670                 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
    671                 host_vault_policy: Some("desktop".into()),
    672                 uses_protected_store: true,
    673             },
    674             identity: IdentityConfig {
    675                 path: secrets.join("shared/identities/default.json"),
    676             },
    677             signer: SignerConfig {
    678                 backend: SignerBackend::Local,
    679             },
    680             publish: PublishConfig {
    681                 transport: PublishTransport::DirectNostrRelay,
    682                 source: PublishTransportSource::Defaults,
    683                 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
    684             },
    685             relay: RelayConfig {
    686                 urls: Vec::new(),
    687                 publish_policy: RelayPublishPolicy::Any,
    688                 source: RelayConfigSource::Defaults,
    689             },
    690             local: LocalConfig {
    691                 root: data.join("apps/cli/replica"),
    692                 replica_db_path: data.join("apps/cli/replica/replica.sqlite"),
    693                 backups_dir: data.join("apps/cli/replica/backups"),
    694                 exports_dir: data.join("apps/cli/replica/exports"),
    695             },
    696             myc: MycConfig {
    697                 executable: PathBuf::from("myc"),
    698                 status_timeout_ms: 2_000,
    699             },
    700             hyf: HyfConfig {
    701                 enabled: false,
    702                 executable: PathBuf::from("hyfd"),
    703             },
    704             rpc: RpcConfig {
    705                 url: "http://127.0.0.1:7070".into(),
    706             },
    707             rhi: crate::runtime::config::RhiConfig {
    708                 trusted_worker_pubkeys: Vec::new(),
    709             },
    710             capability_bindings: Vec::new(),
    711         }
    712     }
    713 
    714     fn data(entries: &[(&str, &str)]) -> OperationData {
    715         entries
    716             .iter()
    717             .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
    718             .collect::<Map<String, Value>>()
    719     }
    720 }