commit c1f7e5ec3df7480039c4846b5833e58d982e7d6c
parent c115c20527d94d86c529136516877574b08b815c
Author: triesap <tyson@radroots.org>
Date: Wed, 15 Jul 2026 19:21:58 +0000
Adopt generated Runtime Contract V1 CLI host
Replace the hand-authored operation registry and specialized terminal renderer shards with SDK-generated Runtime Contract V1 bindings. Cut the CLI parser to V1 namespaces, enforce V1 idempotency and approval-proof/--yes policy, and remove retired command families without compatibility aliases.
Align runtime-facing next actions, adapter tests, output envelopes, and warning hygiene with generated V1 operation IDs. Validate the catalog exactly against Runtime Contract V1 and reject retired command surfaces from the binary.
Diffstat:
92 files changed, 2295 insertions(+), 23197 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3625,6 +3625,7 @@ dependencies = [
"radroots_replica_store",
"radroots_replica_sync",
"radroots_runtime",
+ "radroots_runtime_contract_v1",
"radroots_runtime_paths",
"radroots_runtime_store",
"radroots_sdk",
diff --git a/Cargo.toml b/Cargo.toml
@@ -48,6 +48,7 @@ radroots_transport_nostr = { path = "../lib/crates/transport_nostr", default-fea
"runtime-tokio",
] }
radroots_runtime = { path = "../lib/crates/runtime" }
+radroots_runtime_contract_v1 = { path = "../sdk/crates/runtime_contract_v1" }
radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
radroots_sdk = { path = "../sdk/crates/sdk", features = ["local-runtime-radrootsd-execution", "transport-nostr-runtime"] }
radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] }
diff --git a/src/cli/account.rs b/src/cli/account.rs
@@ -12,11 +12,9 @@ pub struct AccountArgs {
pub enum AccountCommand {
Create,
Import(AccountImportArgs),
- AttachSecret(AccountAttachSecretArgs),
- Get(AccountGetArgs),
+ Select(AccountSelectorArgs),
List,
Remove(AccountSelectorArgs),
- Selection(AccountSelectionArgs),
}
#[derive(Debug, Clone, Args)]
@@ -27,32 +25,6 @@ pub struct AccountImportArgs {
}
#[derive(Debug, Clone, Args)]
-pub struct AccountAttachSecretArgs {
- pub selector: Option<String>,
- pub path: Option<PathBuf>,
- #[arg(long, action = clap::ArgAction::SetTrue)]
- pub default: bool,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountGetArgs {
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
pub struct AccountSelectorArgs {
pub selector: Option<String>,
}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountSelectionArgs {
- #[command(subcommand)]
- pub command: AccountSelectionCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum AccountSelectionCommand {
- Get,
- Update(AccountSelectorArgs),
- Clear,
-}
diff --git a/src/cli/basket.rs b/src/cli/basket.rs
@@ -12,9 +12,7 @@ pub enum BasketCommand {
Get(BasketKeyArgs),
List,
Item(BasketItemArgs),
- Adjustment(BasketAdjustmentArgs),
- Validate(BasketKeyArgs),
- Quote(BasketQuoteArgs),
+ Quote(BasketKeyArgs),
}
#[derive(Debug, Clone, Args)]
@@ -49,40 +47,6 @@ pub enum BasketItemCommand {
}
#[derive(Debug, Clone, Args)]
-pub struct BasketAdjustmentArgs {
- #[command(subcommand)]
- pub command: BasketAdjustmentCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum BasketAdjustmentCommand {
- Add(BasketAdjustmentAddArgs),
- Remove(BasketAdjustmentRemoveArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketAdjustmentAddArgs {
- pub basket_id: Option<String>,
- #[arg(long)]
- pub id: Option<String>,
- #[arg(long)]
- pub effect: Option<String>,
- #[arg(long)]
- pub amount: Option<String>,
- #[arg(long)]
- pub currency: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketAdjustmentRemoveArgs {
- pub basket_id: Option<String>,
- #[arg(long)]
- pub id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
pub struct BasketItemMutationArgs {
pub basket_id: Option<String>,
#[arg(long = "item-id")]
@@ -102,14 +66,3 @@ pub struct BasketItemRemoveArgs {
pub basket_id: Option<String>,
pub item_id: Option<String>,
}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketQuoteArgs {
- #[command(subcommand)]
- pub command: BasketQuoteCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum BasketQuoteCommand {
- Create(BasketKeyArgs),
-}
diff --git a/src/cli/config.rs b/src/cli/config.rs
@@ -1,12 +0,0 @@
-use clap::{Args, Subcommand};
-
-#[derive(Debug, Clone, Args)]
-pub struct ConfigArgs {
- #[command(subcommand)]
- pub command: ConfigCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum ConfigCommand {
- Get,
-}
diff --git a/src/cli/diagnostics.rs b/src/cli/diagnostics.rs
@@ -0,0 +1,12 @@
+use clap::{Args, Subcommand};
+
+#[derive(Debug, Clone, Args)]
+pub struct DiagnosticsArgs {
+ #[command(subcommand)]
+ pub command: DiagnosticsCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum DiagnosticsCommand {
+ Inspect,
+}
diff --git a/src/cli/farm.rs b/src/cli/farm.rs
@@ -1,4 +1,4 @@
-use clap::{ArgGroup, Args, Subcommand};
+use clap::{Args, Subcommand};
#[derive(Debug, Clone, Args)]
pub struct FarmArgs {
@@ -8,14 +8,11 @@ pub struct FarmArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum FarmCommand {
- Create(FarmCreateArgs),
- Get,
- Rebind(FarmRebindArgs),
- Profile(FarmProfileArgs),
- Location(FarmLocationArgs),
- Fulfillment(FarmFulfillmentArgs),
- Readiness(FarmReadinessArgs),
+ Create(Box<FarmCreateArgs>),
+ Update(FarmUpdateArgs),
Publish,
+ Get,
+ List,
}
#[derive(Debug, Clone, Args)]
@@ -49,100 +46,9 @@ pub struct FarmCreateArgs {
}
#[derive(Debug, Clone, Args)]
-pub struct FarmRebindArgs {
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmProfileArgs {
- #[command(subcommand)]
- pub command: FarmProfileCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmProfileCommand {
- Update(FarmProfileUpdateArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmProfileUpdateArgs {
+pub struct FarmUpdateArgs {
#[arg(long)]
pub field: Option<String>,
#[arg(long)]
pub value: Option<String>,
}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmLocationArgs {
- #[command(subcommand)]
- pub command: FarmLocationCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmLocationCommand {
- Set(FarmLocationSetArgs),
- Get(FarmLocationKeyArgs),
- Clear(FarmLocationKeyArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-#[command(group(
- ArgGroup::new("location_mode")
- .args(["lat", "city", "query", "geonames_id"])
- .required(true)
- .multiple(false)
-))]
-pub struct FarmLocationSetArgs {
- #[arg(long, allow_negative_numbers = true, requires = "lng")]
- pub lat: Option<f64>,
- #[arg(long, allow_negative_numbers = true, requires = "lat")]
- pub lng: Option<f64>,
- #[arg(long = "farm-d-tag")]
- pub farm_d_tag: Option<String>,
- #[arg(long)]
- pub city: Option<String>,
- #[arg(long, requires = "city", conflicts_with_all = ["lat", "query", "geonames_id"])]
- pub region: Option<String>,
- #[arg(long, requires = "city", conflicts_with_all = ["lat", "query", "geonames_id"])]
- pub country: Option<String>,
- #[arg(long)]
- pub query: Option<String>,
- #[arg(long = "geonames-id", value_parser = clap::value_parser!(i64).range(1..))]
- pub geonames_id: Option<i64>,
- #[arg(long)]
- pub label: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmLocationKeyArgs {
- #[arg(long = "farm-d-tag")]
- pub farm_d_tag: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmFulfillmentArgs {
- #[command(subcommand)]
- pub command: FarmFulfillmentCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmFulfillmentCommand {
- Update(FarmFulfillmentUpdateArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmFulfillmentUpdateArgs {
- #[arg(long)]
- pub value: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmReadinessArgs {
- #[command(subcommand)]
- pub command: FarmReadinessCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum FarmReadinessCommand {
- Check,
-}
diff --git a/src/cli/global.rs b/src/cli/global.rs
@@ -232,7 +232,7 @@ pub struct OrderDraftAdjustmentArgs {
}
#[derive(Debug, Clone)]
-pub struct TradeSubmitArgs {
+pub struct TradeRequestArgs {
pub key: String,
pub idempotency_key: Option<String>,
pub confirm_public_note: bool,
diff --git a/src/cli/health.rs b/src/cli/health.rs
@@ -8,28 +8,5 @@ pub struct HealthArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum HealthCommand {
- Status(HealthStatusArgs),
- Check(HealthCheckArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct HealthStatusArgs {
- #[command(subcommand)]
- pub command: HealthStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum HealthStatusCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct HealthCheckArgs {
- #[command(subcommand)]
- pub command: HealthCheckCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum HealthCheckCommand {
- Run,
+ Inspect,
}
diff --git a/src/cli/input.rs b/src/cli/input.rs
@@ -18,7 +18,7 @@ pub fn runtime_invocation_args_from_target(args: &TargetCliArgs) -> RuntimeInvoc
trace: args.trace,
dry_run: args.dry_run,
no_input: args.no_input,
- yes: false,
+ yes: args.yes,
log_filter: None,
log_dir: None,
log_stdout: false,
@@ -40,13 +40,10 @@ pub fn operation_id_from_target(args: &TargetCliArgs) -> &'static str {
pub fn target_operation_input(command: &TargetCommand) -> OperationData {
use crate::cli::{
- AccountCommand, AccountSelectionCommand, BasketAdjustmentCommand, BasketCommand,
- BasketItemCommand, BasketQuoteCommand, FarmCommand, FarmFulfillmentCommand,
- FarmLocationCommand, FarmProfileCommand, ListingAppCommand, ListingCommand, MarketCommand,
- MarketListingCommand, MarketProductCommand, MeshCommand, MeshScopeCommand,
- StoreBackupCommand, StoreCommand, TradeAppCommand, TradeCommand, TradeEventCommand,
- TradeStatusCommand, TransportCommand, TransportOutboxCommand, TransportProfileCommand,
- ValidationCommand, ValidationReceiptCommand,
+ AccountCommand, BasketCommand, BasketItemCommand, FarmCommand, ListingCommand,
+ MarketCommand, StoreCommand, TradeCommand, TransportCapabilityCommand, TransportCommand,
+ TransportConfigCommand, TransportDeliveryCommand, ValidationCommand,
+ ValidationReceiptCommand,
};
let mut input = OperationData::new();
@@ -58,21 +55,8 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
input.insert("default".to_owned(), Value::Bool(true));
}
}
- AccountCommand::AttachSecret(args) => {
- insert_string(&mut input, "selector", &args.selector);
- insert_path(&mut input, "path", &args.path);
- if args.default {
- input.insert("default".to_owned(), Value::Bool(true));
- }
- }
- AccountCommand::Get(args) => insert_string(&mut input, "selector", &args.selector),
+ AccountCommand::Select(args) => insert_string(&mut input, "selector", &args.selector),
AccountCommand::Remove(args) => insert_string(&mut input, "selector", &args.selector),
- AccountCommand::Selection(args) => match &args.command {
- AccountSelectionCommand::Update(args) => {
- insert_string(&mut input, "selector", &args.selector)
- }
- AccountSelectionCommand::Get | AccountSelectionCommand::Clear => {}
- },
AccountCommand::Create | AccountCommand::List => {}
},
TargetCommand::Farm(args) => match &args.command {
@@ -91,46 +75,11 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
insert_string(&mut input, "geohash", &args.geohash);
insert_string(&mut input, "delivery_method", &args.delivery_method);
}
- FarmCommand::Rebind(args) => {
- insert_string(&mut input, "selector", &args.selector);
+ FarmCommand::Update(args) => {
+ insert_string(&mut input, "field", &args.field);
+ insert_string(&mut input, "value", &args.value);
}
- FarmCommand::Profile(args) => match &args.command {
- FarmProfileCommand::Update(args) => {
- insert_string(&mut input, "field", &args.field);
- insert_string(&mut input, "value", &args.value);
- }
- },
- FarmCommand::Location(args) => match &args.command {
- FarmLocationCommand::Set(args) => {
- if let Some(latitude) = args.lat {
- insert_number(&mut input, "lat", latitude);
- }
- if let Some(longitude) = args.lng {
- insert_number(&mut input, "lng", longitude);
- }
- insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
- insert_string(&mut input, "city", &args.city);
- insert_string(&mut input, "region", &args.region);
- insert_string(&mut input, "country", &args.country);
- insert_string(&mut input, "query", &args.query);
- if let Some(geonames_id) = args.geonames_id {
- input.insert(
- "geonames_id".to_owned(),
- Value::Number(serde_json::Number::from(geonames_id)),
- );
- }
- insert_string(&mut input, "label", &args.label);
- }
- FarmLocationCommand::Get(args) | FarmLocationCommand::Clear(args) => {
- insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
- }
- },
- FarmCommand::Fulfillment(args) => match &args.command {
- FarmFulfillmentCommand::Update(args) => {
- insert_string(&mut input, "value", &args.value);
- }
- },
- FarmCommand::Get | FarmCommand::Readiness(_) | FarmCommand::Publish => {}
+ FarmCommand::Get | FarmCommand::List | FarmCommand::Publish => {}
},
TargetCommand::Listing(args) => match &args.command {
ListingCommand::Create(args) => {
@@ -156,47 +105,28 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
insert_string(&mut input, "discount_currency", &args.discount_currency);
}
ListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
- ListingCommand::App(args) => match &args.command {
- ListingAppCommand::Export(args) => {
- insert_string(&mut input, "record_id", &args.record_id);
- insert_path(&mut input, "output", &args.output);
- }
- ListingAppCommand::List => {}
- },
ListingCommand::Update(args)
- | ListingCommand::Validate(args)
| ListingCommand::Publish(args)
- | ListingCommand::Archive(args) => insert_path(&mut input, "file", &args.file),
- ListingCommand::Rebind(args) => {
- insert_path(&mut input, "file", &args.file);
- insert_string(&mut input, "selector", &args.selector);
- insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
- }
+ | ListingCommand::Pause(args)
+ | ListingCommand::Withdraw(args) => insert_path(&mut input, "file", &args.file),
ListingCommand::List => {}
},
TargetCommand::Market(args) => match &args.command {
- MarketCommand::Product(product) => match &product.command {
- MarketProductCommand::Search(args) => {
- insert_string_array(&mut input, "query", args.query.as_slice())
- }
- },
- MarketCommand::Listing(listing) => match &listing.command {
- MarketListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
- },
- MarketCommand::Refresh => {}
+ MarketCommand::Pull => {}
+ MarketCommand::Search(args) => {
+ insert_string_array(&mut input, "query", args.query.as_slice())
+ }
+ MarketCommand::Get(args) => insert_string(&mut input, "key", &args.key),
},
TargetCommand::Store(args) => match &args.command {
- StoreCommand::Backup(backup) => match &backup.command {
- StoreBackupCommand::Restore(args) => {
- insert_path(&mut input, "source", &Some(args.source.clone()));
- insert_path(&mut input, "destination", &args.destination);
- if args.overwrite {
- input.insert("overwrite".to_owned(), Value::Bool(true));
- }
+ StoreCommand::Restore(args) => {
+ insert_path(&mut input, "source", &Some(args.source.clone()));
+ insert_path(&mut input, "destination", &args.destination);
+ if args.overwrite {
+ input.insert("overwrite".to_owned(), Value::Bool(true));
}
- StoreBackupCommand::Create => {}
- },
- StoreCommand::Init | StoreCommand::Status(_) | StoreCommand::Export => {}
+ }
+ StoreCommand::Inspect | StoreCommand::Backup => {}
},
TargetCommand::Basket(args) => match &args.command {
BasketCommand::Create(args) => {
@@ -206,9 +136,7 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
insert_string(&mut input, "bin_id", &args.bin_id);
insert_string(&mut input, "quantity", &args.quantity);
}
- BasketCommand::Get(args) | BasketCommand::Validate(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id)
- }
+ BasketCommand::Get(args) => insert_string(&mut input, "basket_id", &args.basket_id),
BasketCommand::Item(item) => match &item.command {
BasketItemCommand::Add(args) | BasketItemCommand::Update(args) => {
insert_string(&mut input, "basket_id", &args.basket_id);
@@ -223,44 +151,15 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
insert_string(&mut input, "item_id", &args.item_id);
}
},
- BasketCommand::Adjustment(adjustment) => match &adjustment.command {
- BasketAdjustmentCommand::Add(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "id", &args.id);
- insert_string(&mut input, "effect", &args.effect);
- insert_string(&mut input, "amount", &args.amount);
- insert_string(&mut input, "currency", &args.currency);
- insert_string(&mut input, "reason", &args.reason);
- }
- BasketAdjustmentCommand::Remove(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "id", &args.id);
- }
- },
- BasketCommand::Quote(quote) => match "e.command {
- BasketQuoteCommand::Create(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id)
- }
- },
+ BasketCommand::Quote(args) => insert_string(&mut input, "basket_id", &args.basket_id),
BasketCommand::List => {}
},
TargetCommand::Trade(args) => match &args.command {
- TradeCommand::Submit(args) => {
+ TradeCommand::Request(args) => {
insert_string(&mut input, "trade_id", &args.trade_id);
insert_bool(&mut input, "confirm_public_note", args.confirm_public_note);
}
TradeCommand::Get(args) => insert_string(&mut input, "trade_id", &args.trade_id),
- TradeCommand::App(args) => match &args.command {
- TradeAppCommand::Export(args) => {
- insert_string(&mut input, "record_id", &args.record_id);
- insert_path(&mut input, "output", &args.output);
- }
- TradeAppCommand::List => {}
- },
- TradeCommand::Rebind(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id);
- insert_string(&mut input, "selector", &args.selector);
- }
TradeCommand::Accept(args) => insert_string(&mut input, "trade_id", &args.trade_id),
TradeCommand::Decline(args) => {
insert_string(&mut input, "trade_id", &args.trade_id);
@@ -272,31 +171,19 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
insert_string(&mut input, "reason", &args.reason);
insert_bool(&mut input, "confirm_public_note", args.confirm_public_note);
}
- TradeCommand::Status(status) => match &status.command {
- TradeStatusCommand::Get(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id)
- }
- },
- TradeCommand::Event(event) => match &event.command {
- TradeEventCommand::List(args) | TradeEventCommand::Watch(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id)
- }
- },
TradeCommand::List => {}
},
TargetCommand::Validation(args) => match &args.command {
+ ValidationCommand::Status => {}
ValidationCommand::Receipt(receipt) => match &receipt.command {
ValidationReceiptCommand::Get(args) | ValidationReceiptCommand::Verify(args) => {
insert_string(&mut input, "receipt_event_id", &args.receipt_event_id);
}
- ValidationReceiptCommand::List(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id);
- }
},
},
TargetCommand::Transport(args) => match &args.command {
- TransportCommand::Profile(profile) => match &profile.command {
- TransportProfileCommand::Set(args) => {
+ TransportCommand::Config(config) => match &config.command {
+ TransportConfigCommand::Update(args) => {
input.insert(
"kind".to_owned(),
Value::String(args.kind.as_str().to_owned()),
@@ -315,25 +202,15 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
&args.reticulum_agent_endpoint,
);
}
- TransportProfileCommand::Get => {}
+ TransportConfigCommand::Inspect => {}
},
- TransportCommand::Status => {}
- TransportCommand::Outbox(outbox) => match outbox.command {
- TransportOutboxCommand::Status | TransportOutboxCommand::Push => {}
+ TransportCommand::Capability(capability) => match capability.command {
+ TransportCapabilityCommand::List => {}
},
- },
- TargetCommand::Mesh(args) => match &args.command {
- MeshCommand::Scope(scope) => match &scope.command {
- MeshScopeCommand::Set(args) => {
- input.insert(
- "scope".to_owned(),
- Value::String(args.scope.as_str().to_owned()),
- );
- }
- MeshScopeCommand::Get => {}
+ TransportCommand::Delivery(delivery) => match delivery.command {
+ TransportDeliveryCommand::Inspect | TransportDeliveryCommand::Retry => {}
},
- MeshCommand::Status => {}
- MeshCommand::Policy(_) => {}
+ TransportCommand::Status(_) => {}
},
_ => {}
}
@@ -363,12 +240,6 @@ fn insert_string_array(input: &mut OperationData, key: &str, values: &[String])
}
}
-fn insert_number(input: &mut OperationData, key: &str, value: f64) {
- if let Some(number) = serde_json::Number::from_f64(value) {
- input.insert(key.to_owned(), Value::Number(number));
- }
-}
-
fn insert_bool(input: &mut OperationData, key: &str, value: bool) {
if value {
input.insert(key.to_owned(), Value::Bool(true));
diff --git a/src/cli/listing.rs b/src/cli/listing.rs
@@ -10,15 +10,13 @@ pub struct ListingArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum ListingCommand {
- Create(ListingCreateArgs),
- Get(LookupArgs),
- List,
- App(ListingAppArgs),
+ Create(Box<ListingCreateArgs>),
Update(FileArgs),
- Validate(FileArgs),
- Rebind(ListingRebindArgs),
Publish(FileArgs),
- Archive(FileArgs),
+ Pause(FileArgs),
+ Withdraw(FileArgs),
+ Get(LookupArgs),
+ List,
}
#[derive(Debug, Clone, Args)]
@@ -71,33 +69,6 @@ pub struct FileArgs {
}
#[derive(Debug, Clone, Args)]
-pub struct ListingAppArgs {
- #[command(subcommand)]
- pub command: ListingAppCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum ListingAppCommand {
- List,
- Export(ListingAppExportArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingAppExportArgs {
- pub record_id: Option<String>,
- #[arg(long)]
- pub output: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingRebindArgs {
- pub file: Option<PathBuf>,
- pub selector: Option<String>,
- #[arg(long = "farm-d-tag")]
- pub farm_d_tag: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
pub struct LookupArgs {
pub key: Option<String>,
}
diff --git a/src/cli/market.rs b/src/cli/market.rs
@@ -10,30 +10,8 @@ pub struct MarketArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum MarketCommand {
- Refresh,
- Product(MarketProductArgs),
- Listing(MarketListingArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MarketProductArgs {
- #[command(subcommand)]
- pub command: MarketProductCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MarketProductCommand {
+ Pull,
Search(QueryArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MarketListingArgs {
- #[command(subcommand)]
- pub command: MarketListingCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MarketListingCommand {
Get(LookupArgs),
}
diff --git a/src/cli/mesh.rs b/src/cli/mesh.rs
@@ -1,58 +0,0 @@
-use clap::{Args, Subcommand, ValueEnum};
-
-#[derive(Debug, Clone, Args)]
-pub struct MeshArgs {
- #[command(subcommand)]
- pub command: MeshCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MeshCommand {
- Scope(MeshScopeArgs),
- Status,
- Policy(MeshPolicyArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MeshScopeArgs {
- #[command(subcommand)]
- pub command: MeshScopeCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MeshScopeCommand {
- Get,
- Set(MeshScopeSetArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MeshScopeSetArgs {
- #[arg(long = "scope", value_enum)]
- pub scope: MeshScopeArg,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
-pub enum MeshScopeArg {
- Disabled,
- Local,
-}
-
-impl MeshScopeArg {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Disabled => "disabled",
- Self::Local => "local",
- }
- }
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MeshPolicyArgs {
- #[command(subcommand)]
- pub command: MeshPolicyCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum MeshPolicyCommand {
- Check,
-}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
@@ -2,36 +2,34 @@ pub mod global;
pub mod account;
pub mod basket;
-pub mod config;
+pub mod diagnostics;
pub mod farm;
pub mod health;
pub mod input;
pub mod listing;
pub mod market;
-pub mod mesh;
+pub mod profile;
pub mod signer;
pub mod store;
pub mod sync;
pub mod trade;
pub mod transport;
pub mod validation;
-pub mod workspace;
pub use account::*;
pub use basket::*;
-pub use config::*;
+pub use diagnostics::*;
pub use farm::*;
pub use health::*;
pub use listing::*;
pub use market::*;
-pub use mesh::*;
+pub use profile::*;
pub use signer::*;
pub use store::*;
pub use sync::*;
pub use trade::*;
pub use transport::*;
pub use validation::*;
-pub use workspace::*;
use clap::{ArgAction, Parser, Subcommand, ValueEnum};
@@ -64,8 +62,10 @@ pub struct TargetCliArgs {
pub idempotency_key: Option<String>,
#[arg(long = "correlation-id", global = true)]
pub correlation_id: Option<String>,
- #[arg(long = "approval-token", global = true)]
- pub approval_token: Option<String>,
+ #[arg(long = "yes", global = true, action = ArgAction::SetTrue)]
+ pub yes: bool,
+ #[arg(long = "approval-proof", global = true)]
+ pub approval_proof: Option<String>,
#[arg(long = "no-input", global = true, action = ArgAction::SetTrue)]
pub no_input: bool,
#[arg(long = "quiet", global = true, action = ArgAction::SetTrue)]
@@ -80,20 +80,16 @@ pub struct TargetCliArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum TargetCommand {
- #[command(about = "Inspect and initialize workspace state.")]
- Workspace(WorkspaceArgs),
+ #[command(about = "Inspect and reset runtime profile state.")]
+ Profile(ProfileArgs),
#[command(about = "Inspect local readiness and mode-specific recovery steps.")]
Health(HealthArgs),
- #[command(about = "Show effective configuration and publish-plane readiness.")]
- Config(ConfigArgs),
#[command(about = "Manage local signer accounts and custody.")]
Account(AccountArgs),
#[command(about = "Inspect signer readiness for local relay writes.")]
Signer(SignerArgs),
#[command(about = "Manage transport profiles and outbox delivery.")]
Transport(TransportArgs),
- #[command(about = "Inspect mesh scope and Reticulum admission policy.")]
- Mesh(MeshArgs),
#[command(about = "Initialize and inspect the local replica store.")]
Store(StoreArgs),
#[command(about = "Read from relay events into the local replica.")]
@@ -110,126 +106,76 @@ pub enum TargetCommand {
Trade(TradeArgs),
#[command(about = "Inspect validation receipts and proof state.")]
Validation(ValidationArgs),
+ #[command(about = "Inspect runtime diagnostics.")]
+ Diagnostics(DiagnosticsArgs),
}
impl TargetCommand {
pub fn operation_id(&self) -> &'static str {
match self {
- Self::Workspace(args) => match args.command {
- WorkspaceCommand::Init => "workspace.init",
- WorkspaceCommand::Get => "workspace.get",
+ Self::Profile(args) => match args.command {
+ ProfileCommand::Inspect => "profile.inspect",
+ ProfileCommand::Reset => "profile.reset",
},
- Self::Health(args) => match &args.command {
- HealthCommand::Status(status) => match status.command {
- HealthStatusCommand::Get => "health.status.get",
- },
- HealthCommand::Check(check) => match check.command {
- HealthCheckCommand::Run => "health.check.run",
- },
- },
- Self::Config(args) => match args.command {
- ConfigCommand::Get => "config.get",
+ Self::Health(args) => match args.command {
+ HealthCommand::Inspect => "health.inspect",
},
Self::Account(args) => match &args.command {
AccountCommand::Create => "account.create",
AccountCommand::Import(_) => "account.import",
- AccountCommand::AttachSecret(_) => "account.attach_secret",
- AccountCommand::Get(_) => "account.get",
+ AccountCommand::Select(_) => "account.select",
AccountCommand::List => "account.list",
AccountCommand::Remove(_) => "account.remove",
- AccountCommand::Selection(selection) => match &selection.command {
- AccountSelectionCommand::Get => "account.selection.get",
- AccountSelectionCommand::Update(_) => "account.selection.update",
- AccountSelectionCommand::Clear => "account.selection.clear",
- },
},
- Self::Signer(args) => match &args.command {
- SignerCommand::Status(status) => match status.command {
- SignerStatusCommand::Get => "signer.status.get",
- },
+ Self::Signer(args) => match args.command {
+ SignerCommand::Status => "signer.status",
},
Self::Transport(args) => match &args.command {
- TransportCommand::Profile(profile) => match &profile.command {
- TransportProfileCommand::Get => "transport.profile.get",
- TransportProfileCommand::Set(_) => "transport.profile.set",
+ TransportCommand::Capability(capability) => match capability.command {
+ TransportCapabilityCommand::List => "transport.capability.list",
},
- TransportCommand::Status => "transport.status",
- TransportCommand::Outbox(outbox) => match outbox.command {
- TransportOutboxCommand::Status => "transport.outbox.status",
- TransportOutboxCommand::Push => "transport.outbox.push",
+ TransportCommand::Config(config) => match &config.command {
+ TransportConfigCommand::Inspect => "transport.config.inspect",
+ TransportConfigCommand::Update(_) => "transport.config.update",
},
- },
- Self::Mesh(args) => match &args.command {
- MeshCommand::Scope(scope) => match &scope.command {
- MeshScopeCommand::Get => "mesh.scope.get",
- MeshScopeCommand::Set(_) => "mesh.scope.set",
+ TransportCommand::Status(status) => match status.command {
+ TransportStatusCommand::Inspect => "transport.status.inspect",
},
- MeshCommand::Status => "mesh.status",
- MeshCommand::Policy(policy) => match policy.command {
- MeshPolicyCommand::Check => "mesh.policy.check",
+ TransportCommand::Delivery(delivery) => match delivery.command {
+ TransportDeliveryCommand::Inspect => "transport.delivery.inspect",
+ TransportDeliveryCommand::Retry => "transport.delivery.retry",
},
},
Self::Store(args) => match &args.command {
- StoreCommand::Init => "store.init",
- StoreCommand::Status(status) => match status.command {
- StoreStatusCommand::Get => "store.status.get",
- },
- StoreCommand::Export => "store.export",
- StoreCommand::Backup(backup) => match &backup.command {
- StoreBackupCommand::Create => "store.backup.create",
- StoreBackupCommand::Restore(_) => "store.backup.restore",
- },
+ StoreCommand::Inspect => "store.inspect",
+ StoreCommand::Backup => "store.backup",
+ StoreCommand::Restore(_) => "store.restore",
},
Self::Sync(args) => match &args.command {
- SyncCommand::Status(status) => match status.command {
- SyncStatusCommand::Get => "sync.status.get",
- },
+ SyncCommand::Status => "sync.status",
SyncCommand::Pull => "sync.pull",
SyncCommand::Push => "sync.push",
- SyncCommand::Watch => "sync.watch",
},
Self::Farm(args) => match &args.command {
FarmCommand::Create(_) => "farm.create",
- FarmCommand::Get => "farm.get",
- FarmCommand::Rebind(_) => "farm.rebind",
- FarmCommand::Profile(profile) => match profile.command {
- FarmProfileCommand::Update(_) => "farm.profile.update",
- },
- FarmCommand::Location(location) => match location.command {
- FarmLocationCommand::Set(_) => "farm.location.set",
- FarmLocationCommand::Get(_) => "farm.location.get",
- FarmLocationCommand::Clear(_) => "farm.location.clear",
- },
- FarmCommand::Fulfillment(fulfillment) => match fulfillment.command {
- FarmFulfillmentCommand::Update(_) => "farm.fulfillment.update",
- },
- FarmCommand::Readiness(readiness) => match readiness.command {
- FarmReadinessCommand::Check => "farm.readiness.check",
- },
+ FarmCommand::Update(_) => "farm.update",
FarmCommand::Publish => "farm.publish",
+ FarmCommand::Get => "farm.get",
+ FarmCommand::List => "farm.list",
},
Self::Listing(args) => match &args.command {
ListingCommand::Create(_) => "listing.create",
- ListingCommand::Get(_) => "listing.get",
- ListingCommand::List => "listing.list",
- ListingCommand::App(app) => match &app.command {
- ListingAppCommand::List => "listing.app.list",
- ListingAppCommand::Export(_) => "listing.app.export",
- },
ListingCommand::Update(_) => "listing.update",
- ListingCommand::Validate(_) => "listing.validate",
- ListingCommand::Rebind(_) => "listing.rebind",
ListingCommand::Publish(_) => "listing.publish",
- ListingCommand::Archive(_) => "listing.archive",
+ ListingCommand::Pause(_) => "listing.pause",
+ ListingCommand::Withdraw(_) => "listing.withdraw",
+ ListingCommand::Get(_) => "listing.get",
+ ListingCommand::List => "listing.list",
},
Self::Market(args) => match &args.command {
- MarketCommand::Refresh => "market.refresh",
- MarketCommand::Product(product) => match &product.command {
- MarketProductCommand::Search(_) => "market.product.search",
- },
- MarketCommand::Listing(listing) => match &listing.command {
- MarketListingCommand::Get(_) => "market.listing.get",
- },
+ MarketCommand::Pull => "market.pull",
+ MarketCommand::Search(_) => "market.search",
+ MarketCommand::Get(_) => "market.get",
},
Self::Basket(args) => match &args.command {
BasketCommand::Create(_) => "basket.create",
@@ -240,59 +186,41 @@ impl TargetCommand {
BasketItemCommand::Update(_) => "basket.item.update",
BasketItemCommand::Remove(_) => "basket.item.remove",
},
- BasketCommand::Adjustment(adjustment) => match &adjustment.command {
- BasketAdjustmentCommand::Add(_) => "basket.adjustment.add",
- BasketAdjustmentCommand::Remove(_) => "basket.adjustment.remove",
- },
- BasketCommand::Validate(_) => "basket.validate",
- BasketCommand::Quote(quote) => match quote.command {
- BasketQuoteCommand::Create(_) => "basket.quote.create",
- },
+ BasketCommand::Quote(_) => "basket.quote",
},
Self::Trade(args) => match &args.command {
- TradeCommand::Submit(_) => "trade.submit",
+ TradeCommand::Request(_) => "trade.request",
TradeCommand::Get(_) => "trade.get",
TradeCommand::List => "trade.list",
- TradeCommand::App(app) => match &app.command {
- TradeAppCommand::List => "trade.app.list",
- TradeAppCommand::Export(_) => "trade.app.export",
- },
- TradeCommand::Rebind(_) => "trade.rebind",
TradeCommand::Accept(_) => "trade.accept",
TradeCommand::Decline(_) => "trade.decline",
TradeCommand::Cancel(_) => "trade.cancel",
- TradeCommand::Status(status) => match &status.command {
- TradeStatusCommand::Get(_) => "trade.status.get",
- },
- TradeCommand::Event(event) => match &event.command {
- TradeEventCommand::List(_) => "trade.event.list",
- TradeEventCommand::Watch(_) => "trade.event.watch",
- },
},
Self::Validation(args) => match &args.command {
+ ValidationCommand::Status => "validation.status",
ValidationCommand::Receipt(receipt) => match &receipt.command {
ValidationReceiptCommand::Get(_) => "validation.receipt.get",
- ValidationReceiptCommand::List(_) => "validation.receipt.list",
ValidationReceiptCommand::Verify(_) => "validation.receipt.verify",
},
},
+ Self::Diagnostics(args) => match args.command {
+ DiagnosticsCommand::Inspect => "diagnostics.inspect",
+ },
}
}
}
+
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use clap::{CommandFactory, Parser};
- use super::{
- AccountCommand, FarmCommand, FarmLocationCommand, ListingCommand, TargetCliArgs,
- TargetOutputFormat, TradeCommand, ValidationCommand, ValidationReceiptCommand,
- };
+ use super::{TargetCliArgs, TargetCommand, TargetOutputFormat};
use crate::registry::OPERATION_REGISTRY;
#[test]
- fn target_parser_accepts_every_target_registry_path() {
+ fn target_parser_accepts_every_generated_registry_path() {
for operation in OPERATION_REGISTRY {
let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
.unwrap_or_else(|error| {
@@ -303,19 +231,17 @@ mod tests {
}
#[test]
- fn target_parser_exposes_only_target_top_level_namespaces() {
+ fn target_parser_exposes_only_v1_namespaces() {
let actual = TargetCliArgs::command()
.get_subcommands()
.map(|command| command.get_name().to_owned())
.collect::<BTreeSet<_>>();
let expected = [
- "workspace",
+ "profile",
"health",
- "config",
"account",
"signer",
"transport",
- "mesh",
"store",
"sync",
"farm",
@@ -324,6 +250,7 @@ mod tests {
"basket",
"trade",
"validation",
+ "diagnostics",
]
.into_iter()
.map(str::to_owned)
@@ -333,7 +260,7 @@ mod tests {
}
#[test]
- fn target_global_flags_parse() {
+ fn target_global_flags_parse_v1_approval_inputs() {
let parsed = TargetCliArgs::try_parse_from([
"radroots",
"--format",
@@ -343,15 +270,16 @@ mod tests {
"--offline",
"--dry-run",
"--idempotency-key",
- "idem_test",
+ "018f3d99-7d35-7c0c-8a0f-7f3b645abcde",
"--correlation-id",
"corr_test",
- "--approval-token",
- "approval_test",
+ "--yes",
+ "--approval-proof",
+ "{\"operation_id\":\"profile.reset\"}",
"--no-input",
"--quiet",
- "workspace",
- "get",
+ "profile",
+ "inspect",
])
.expect("target args parse");
@@ -359,404 +287,92 @@ mod tests {
assert_eq!(parsed.account_id.as_deref(), Some("acct_test"));
assert!(parsed.offline);
assert!(parsed.dry_run);
- assert_eq!(parsed.idempotency_key.as_deref(), Some("idem_test"));
+ assert_eq!(
+ parsed.idempotency_key.as_deref(),
+ Some("018f3d99-7d35-7c0c-8a0f-7f3b645abcde")
+ );
assert_eq!(parsed.correlation_id.as_deref(), Some("corr_test"));
- assert_eq!(parsed.approval_token.as_deref(), Some("approval_test"));
+ assert!(parsed.yes);
+ assert!(parsed.approval_proof.is_some());
assert!(parsed.no_input);
assert!(parsed.quiet);
- assert_eq!(parsed.command.operation_id(), "workspace.get");
- }
-
- #[test]
- fn target_parser_rejects_removed_no_color_flag() {
- let error = TargetCliArgs::try_parse_from(["radroots", "--no-color", "workspace", "get"])
- .expect_err("removed no-color flag should be rejected");
-
- assert_eq!(error.kind(), clap::error::ErrorKind::UnknownArgument);
- }
-
- #[test]
- fn target_parser_rejects_removed_relay_flag() {
- let error = TargetCliArgs::try_parse_from([
- "radroots",
- "--relay",
- "wss://relay.example.com",
- "workspace",
- "get",
- ])
- .expect_err("removed relay flag should be rejected");
-
- assert_eq!(error.kind(), clap::error::ErrorKind::UnknownArgument);
- }
-
- #[test]
- fn target_parser_accepts_account_attach_secret_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "account",
- "attach-secret",
- "acct_test",
- "identity.json",
- "--default",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "account.attach_secret");
- let crate::cli::TargetCommand::Account(account) = parsed.command else {
- panic!("expected account command")
- };
- let AccountCommand::AttachSecret(args) = account.command else {
- panic!("expected account attach-secret command")
- };
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- assert_eq!(
- args.path.as_ref().map(|path| path.as_os_str()),
- Some(std::ffi::OsStr::new("identity.json"))
- );
- assert!(args.default);
- }
-
- #[test]
- fn target_parser_accepts_farm_rebind_selector() {
- let parsed = TargetCliArgs::try_parse_from(["radroots", "farm", "rebind", "acct_test"])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "farm.rebind");
- let crate::cli::TargetCommand::Farm(farm) = parsed.command else {
- panic!("expected farm command")
- };
- let FarmCommand::Rebind(args) = farm.command else {
- panic!("expected farm rebind command")
- };
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- }
-
- #[test]
- fn target_parser_accepts_negative_farm_location_coordinates() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "farm",
- "location",
- "set",
- "--lat",
- "48.429456",
- "--lng",
- "-123.349786",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "farm.location.set");
- let crate::cli::TargetCommand::Farm(farm) = parsed.command else {
- panic!("expected farm command")
- };
- let FarmCommand::Location(location) = farm.command else {
- panic!("expected farm location command")
- };
- let FarmLocationCommand::Set(args) = location.command else {
- panic!("expected farm location set command")
- };
- assert_eq!(args.lat, Some(48.429456));
- assert_eq!(args.lng, Some(-123.349786));
- }
-
- #[test]
- fn target_parser_accepts_structured_farm_location_city() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "farm",
- "location",
- "set",
- "--city",
- "Victoria",
- "--region",
- "BC",
- "--country",
- "CA",
- "--label",
- "main pickup point",
- ])
- .expect("target args parse");
-
- let crate::cli::TargetCommand::Farm(farm) = parsed.command else {
- panic!("expected farm command")
- };
- let FarmCommand::Location(location) = farm.command else {
- panic!("expected farm location command")
- };
- let FarmLocationCommand::Set(args) = location.command else {
- panic!("expected farm location set command")
- };
- assert_eq!(args.city.as_deref(), Some("Victoria"));
- assert_eq!(args.region.as_deref(), Some("BC"));
- assert_eq!(args.country.as_deref(), Some("CA"));
- assert_eq!(args.label.as_deref(), Some("main pickup point"));
- }
-
- #[test]
- fn target_parser_accepts_query_and_geonames_id_farm_location_modes() {
- let query = TargetCliArgs::try_parse_from([
- "radroots",
- "farm",
- "location",
- "set",
- "--query",
- "Fixture Victoria, BC, CA",
- ])
- .expect("query args parse");
- let crate::cli::TargetCommand::Farm(farm) = query.command else {
- panic!("expected farm command")
- };
- let FarmCommand::Location(location) = farm.command else {
- panic!("expected farm location command")
- };
- let FarmLocationCommand::Set(args) = location.command else {
- panic!("expected farm location set command")
- };
- assert_eq!(args.query.as_deref(), Some("Fixture Victoria, BC, CA"));
-
- let geonames_id = TargetCliArgs::try_parse_from([
- "radroots",
- "farm",
- "location",
- "set",
- "--geonames-id",
- "3004",
- ])
- .expect("geonames id args parse");
- let crate::cli::TargetCommand::Farm(farm) = geonames_id.command else {
- panic!("expected farm command")
- };
- let FarmCommand::Location(location) = farm.command else {
- panic!("expected farm location command")
- };
- let FarmLocationCommand::Set(args) = location.command else {
- panic!("expected farm location set command")
- };
- assert_eq!(args.geonames_id, Some(3004));
+ assert_eq!(parsed.command.operation_id(), "profile.inspect");
}
#[test]
- fn target_parser_rejects_invalid_farm_location_modes() {
- for argv in [
- vec!["radroots", "farm", "location", "set", "--lat", "48.429456"],
+ fn target_parser_rejects_retired_commands_and_global_flags() {
+ let rejected = [
+ vec![
+ "radroots".to_owned(),
+ format!("--approval-{}", "token"),
+ "approve".to_owned(),
+ "profile".to_owned(),
+ "reset".to_owned(),
+ ],
+ vec![
+ "radroots".to_owned(),
+ "workspace".to_owned(),
+ "get".to_owned(),
+ ],
+ vec!["radroots".to_owned(), "config".to_owned(), "get".to_owned()],
vec![
- "radroots",
- "farm",
- "location",
- "set",
- "--lat",
- "48.429456",
- "--lng",
- "-123.349786",
- "--city",
- "Victoria",
+ "radroots".to_owned(),
+ "mesh".to_owned(),
+ "status".to_owned(),
],
vec![
- "radroots",
- "farm",
- "location",
- "set",
- "--query",
- "Victoria, BC",
- "--country",
- "CA",
+ "radroots".to_owned(),
+ "trade".to_owned(),
+ "submit".to_owned(),
],
vec![
- "radroots",
- "farm",
- "location",
- "set",
- "--lookup",
- "geonames",
- "--lat",
- "48.429456",
- "--lng",
- "-123.349786",
+ "radroots".to_owned(),
+ "listing".to_owned(),
+ "archive".to_owned(),
],
- ] {
+ vec![
+ "radroots".to_owned(),
+ "validation".to_owned(),
+ "receipt".to_owned(),
+ "list".to_owned(),
+ ],
+ ];
+
+ for args in rejected {
assert!(
- TargetCliArgs::try_parse_from(argv).is_err(),
- "expected farm location args to fail"
+ TargetCliArgs::try_parse_from(args.iter().map(String::as_str)).is_err(),
+ "{args:?}"
);
}
}
#[test]
- fn target_parser_accepts_listing_rebind_inputs() {
+ fn target_parser_maps_v1_operation_inputs() {
let parsed = TargetCliArgs::try_parse_from([
"radroots",
- "listing",
- "rebind",
- "listing.toml",
- "acct_test",
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
+ "transport",
+ "config",
+ "update",
+ "--kind",
+ "multi-target",
+ "--nostr-relay",
+ "wss://relay.example",
])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "listing.rebind");
- let crate::cli::TargetCommand::Listing(listing) = parsed.command else {
- panic!("expected listing command")
- };
- let ListingCommand::Rebind(args) = listing.command else {
- panic!("expected listing rebind command")
- };
- assert_eq!(
- args.file.as_ref().map(|path| path.as_os_str()),
- Some(std::ffi::OsStr::new("listing.toml"))
- );
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- assert_eq!(args.farm_d_tag.as_deref(), Some("AAAAAAAAAAAAAAAAAAAAAw"));
- }
+ .expect("transport config update parses");
+ assert_eq!(parsed.command.operation_id(), "transport.config.update");
- #[test]
- fn target_parser_accepts_order_rebind_inputs() {
- let parsed =
- TargetCliArgs::try_parse_from(["radroots", "trade", "rebind", "ord_test", "acct_test"])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "trade.rebind");
- let crate::cli::TargetCommand::Trade(trade) = parsed.command else {
- panic!("expected trade command")
- };
- let TradeCommand::Rebind(args) = trade.command else {
- panic!("expected trade rebind command")
+ let TargetCommand::Transport(_) = parsed.command else {
+ panic!("expected transport command")
};
- assert_eq!(args.trade_id.as_deref(), Some("ord_test"));
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- }
- #[test]
- fn target_parser_accepts_order_cancel_reason() {
- let parsed = TargetCliArgs::try_parse_from([
+ let trade = TargetCliArgs::try_parse_from([
"radroots",
"trade",
- "cancel",
- "ord_test",
- "--reason",
- "changed plans",
+ "request",
+ "trade_test",
"--confirm-public-note",
])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "trade.cancel");
- let crate::cli::TargetCommand::Trade(trade) = parsed.command else {
- panic!("expected trade command")
- };
- let TradeCommand::Cancel(args) = trade.command else {
- panic!("expected trade cancel command")
- };
- assert_eq!(args.trade_id.as_deref(), Some("ord_test"));
- assert_eq!(args.reason.as_deref(), Some("changed plans"));
- assert!(args.confirm_public_note);
- }
-
- #[test]
- fn target_parser_accepts_validation_receipt_commands() {
- let get = TargetCliArgs::try_parse_from([
- "radroots",
- "validation",
- "receipt",
- "get",
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- ])
- .expect("target args parse");
- assert_eq!(get.command.operation_id(), "validation.receipt.get");
- let crate::cli::TargetCommand::Validation(validation) = get.command else {
- panic!("expected validation command")
- };
- let ValidationCommand::Receipt(receipt) = validation.command;
- let ValidationReceiptCommand::Get(args) = receipt.command else {
- panic!("expected validation receipt get command")
- };
- assert_eq!(
- args.receipt_event_id.as_deref(),
- Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
- );
-
- let list = TargetCliArgs::try_parse_from([
- "radroots",
- "validation",
- "receipt",
- "list",
- "--trade-id",
- "ord_1",
- ])
- .expect("target args parse");
- assert_eq!(list.command.operation_id(), "validation.receipt.list");
- let crate::cli::TargetCommand::Validation(validation) = list.command else {
- panic!("expected validation command")
- };
- let ValidationCommand::Receipt(receipt) = validation.command;
- let ValidationReceiptCommand::List(args) = receipt.command else {
- panic!("expected validation receipt list command")
- };
- assert_eq!(args.trade_id.as_deref(), Some("ord_1"));
-
- let verify = TargetCliArgs::try_parse_from([
- "radroots",
- "validation",
- "receipt",
- "verify",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- ])
- .expect("target args parse");
- assert_eq!(verify.command.operation_id(), "validation.receipt.verify");
- }
-
- #[test]
- fn target_parser_rejects_removed_global_flags() {
- let rejected = [
- vec!["radroots", "--output", "json", "config", "get"],
- vec!["radroots", "--json", "config", "get"],
- vec!["radroots", "--ndjson", "config", "get"],
- vec!["radroots", "--yes", "config", "get"],
- vec!["radroots", "--non-interactive", "config", "get"],
- vec!["radroots", "--signer", "myc", "config", "get"],
- vec!["radroots", "--farm-id", "farm_test", "config", "get"],
- vec!["radroots", "--profile", "repo_local", "config", "get"],
- vec![
- "radroots",
- "--signer-session-id",
- "sess_test",
- "config",
- "get",
- ],
- ];
-
- for args in rejected {
- assert!(TargetCliArgs::try_parse_from(args).is_err());
- }
- }
-
- #[test]
- fn target_parser_rejects_removed_top_level_commands() {
- for command in [
- "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc",
- ] {
- assert!(TargetCliArgs::try_parse_from(["radroots", command]).is_err());
- }
- }
-
- #[test]
- fn target_parser_rejects_deferred_namespaces() {
- for command in ["product", "message", "approval", "agent"] {
- assert!(TargetCliArgs::try_parse_from(["radroots", command]).is_err());
- }
- }
-
- #[test]
- fn target_parser_rejects_online_offline_conflict() {
- assert!(
- TargetCliArgs::try_parse_from([
- "radroots",
- "--online",
- "--offline",
- "health",
- "status",
- "get"
- ])
- .is_err()
- );
+ .expect("trade request parses");
+ assert_eq!(trade.command.operation_id(), "trade.request");
}
}
diff --git a/src/cli/profile.rs b/src/cli/profile.rs
@@ -0,0 +1,13 @@
+use clap::{Args, Subcommand};
+
+#[derive(Debug, Clone, Args)]
+pub struct ProfileArgs {
+ #[command(subcommand)]
+ pub command: ProfileCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum ProfileCommand {
+ Inspect,
+ Reset,
+}
diff --git a/src/cli/signer.rs b/src/cli/signer.rs
@@ -8,16 +8,5 @@ pub struct SignerArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum SignerCommand {
- Status(SignerStatusArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct SignerStatusArgs {
- #[command(subcommand)]
- pub command: SignerStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum SignerStatusCommand {
- Get,
+ Status,
}
diff --git a/src/cli/store.rs b/src/cli/store.rs
@@ -10,37 +10,13 @@ pub struct StoreArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum StoreCommand {
- Init,
- Status(StoreStatusArgs),
- Export,
- Backup(StoreBackupArgs),
+ Inspect,
+ Backup,
+ Restore(StoreRestoreArgs),
}
#[derive(Debug, Clone, Args)]
-pub struct StoreStatusArgs {
- #[command(subcommand)]
- pub command: StoreStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum StoreStatusCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct StoreBackupArgs {
- #[command(subcommand)]
- pub command: StoreBackupCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum StoreBackupCommand {
- Create,
- Restore(StoreBackupRestoreArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct StoreBackupRestoreArgs {
+pub struct StoreRestoreArgs {
pub source: PathBuf,
#[arg(long = "destination")]
pub destination: Option<PathBuf>,
diff --git a/src/cli/sync.rs b/src/cli/sync.rs
@@ -8,19 +8,7 @@ pub struct SyncArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum SyncCommand {
- Status(SyncStatusArgs),
+ Status,
Pull,
Push,
- Watch,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct SyncStatusArgs {
- #[command(subcommand)]
- pub command: SyncStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum SyncStatusCommand {
- Get,
}
diff --git a/src/cli/trade.rs b/src/cli/trade.rs
@@ -1,5 +1,3 @@
-use std::path::PathBuf;
-
use clap::{Args, Subcommand};
#[derive(Debug, Clone, Args)]
@@ -10,20 +8,16 @@ pub struct TradeArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum TradeCommand {
- Submit(TradeSubmitArgs),
+ Request(TradeRequestArgs),
Get(TradeKeyArgs),
List,
- App(TradeAppArgs),
- Rebind(TradeRebindArgs),
Accept(TradeKeyArgs),
Decline(TradeDeclineArgs),
Cancel(TradeCancelArgs),
- Status(TradeStatusArgs),
- Event(TradeEventArgs),
}
#[derive(Debug, Clone, Args)]
-pub struct TradeSubmitArgs {
+pub struct TradeRequestArgs {
pub trade_id: Option<String>,
#[arg(long)]
pub confirm_public_note: bool,
@@ -35,31 +29,6 @@ pub struct TradeKeyArgs {
}
#[derive(Debug, Clone, Args)]
-pub struct TradeAppArgs {
- #[command(subcommand)]
- pub command: TradeAppCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum TradeAppCommand {
- List,
- Export(TradeAppExportArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct TradeAppExportArgs {
- pub record_id: Option<String>,
- #[arg(long)]
- pub output: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct TradeRebindArgs {
- pub trade_id: Option<String>,
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
pub struct TradeDeclineArgs {
pub trade_id: Option<String>,
#[arg(long)]
@@ -76,26 +45,3 @@ pub struct TradeCancelArgs {
#[arg(long)]
pub confirm_public_note: bool,
}
-
-#[derive(Debug, Clone, Args)]
-pub struct TradeStatusArgs {
- #[command(subcommand)]
- pub command: TradeStatusCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum TradeStatusCommand {
- Get(TradeKeyArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct TradeEventArgs {
- #[command(subcommand)]
- pub command: TradeEventCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum TradeEventCommand {
- List(TradeKeyArgs),
- Watch(TradeKeyArgs),
-}
diff --git a/src/cli/transport.rs b/src/cli/transport.rs
@@ -8,25 +8,60 @@ pub struct TransportArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum TransportCommand {
- Profile(TransportProfileArgs),
- Status,
- Outbox(TransportOutboxArgs),
+ Capability(TransportCapabilityArgs),
+ Config(TransportConfigArgs),
+ Status(TransportStatusArgs),
+ Delivery(TransportDeliveryArgs),
}
#[derive(Debug, Clone, Args)]
-pub struct TransportProfileArgs {
+pub struct TransportCapabilityArgs {
#[command(subcommand)]
- pub command: TransportProfileCommand,
+ pub command: TransportCapabilityCommand,
}
#[derive(Debug, Clone, Subcommand)]
-pub enum TransportProfileCommand {
- Get,
- Set(TransportProfileSetArgs),
+pub enum TransportCapabilityCommand {
+ List,
}
#[derive(Debug, Clone, Args)]
-pub struct TransportProfileSetArgs {
+pub struct TransportConfigArgs {
+ #[command(subcommand)]
+ pub command: TransportConfigCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum TransportConfigCommand {
+ Inspect,
+ Update(TransportConfigUpdateArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportStatusArgs {
+ #[command(subcommand)]
+ pub command: TransportStatusCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum TransportStatusCommand {
+ Inspect,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportDeliveryArgs {
+ #[command(subcommand)]
+ pub command: TransportDeliveryCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum TransportDeliveryCommand {
+ Inspect,
+ Retry,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportConfigUpdateArgs {
#[arg(long = "kind", value_enum)]
pub kind: TransportProfileKindArg,
#[arg(long = "nostr-relay")]
@@ -72,15 +107,3 @@ impl ReticulumBehaviorArg {
}
}
}
-
-#[derive(Debug, Clone, Args)]
-pub struct TransportOutboxArgs {
- #[command(subcommand)]
- pub command: TransportOutboxCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum TransportOutboxCommand {
- Status,
- Push,
-}
diff --git a/src/cli/validation.rs b/src/cli/validation.rs
@@ -1,5 +1,3 @@
-use std::path::PathBuf;
-
use clap::{Args, Subcommand};
#[derive(Debug, Clone, Args)]
@@ -10,6 +8,7 @@ pub struct ValidationArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum ValidationCommand {
+ Status,
Receipt(ValidationReceiptArgs),
}
@@ -22,7 +21,6 @@ pub struct ValidationReceiptArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum ValidationReceiptCommand {
Get(ValidationReceiptEventArgs),
- List(ValidationReceiptListArgs),
Verify(ValidationReceiptEventArgs),
}
@@ -30,15 +28,3 @@ pub enum ValidationReceiptCommand {
pub struct ValidationReceiptEventArgs {
pub receipt_event_id: Option<String>,
}
-
-#[derive(Debug, Clone, Args)]
-pub struct ValidationReceiptListArgs {
- #[arg(long = "trade-id")]
- pub trade_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct PathOutputArgs {
- #[arg(long)]
- pub output: Option<PathBuf>,
-}
diff --git a/src/cli/workspace.rs b/src/cli/workspace.rs
@@ -1,13 +0,0 @@
-use clap::{Args, Subcommand};
-
-#[derive(Debug, Clone, Args)]
-pub struct WorkspaceArgs {
- #[command(subcommand)]
- pub command: WorkspaceCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum WorkspaceCommand {
- Init,
- Get,
-}
diff --git a/src/generated/runtime_contract_registry.rs b/src/generated/runtime_contract_registry.rs
@@ -0,0 +1,799 @@
+// @generated by radroots_sdk_xtask. Do not edit by hand.
+
+pub const OPERATION_REGISTRY: &[OperationSpec] = &[
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ProfileInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[0],
+ operation_id: "profile.inspect",
+ cli_path: "radroots profile inspect",
+ namespace: "profile",
+ mcp_tool: "profile_inspect",
+ rust_request: "ProfileInspectRequest",
+ rust_result: "ProfileInspectResult",
+ json_kind: "profile.inspect",
+ description: "Inspect the active runtime profile.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ProfileReset,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[1],
+ operation_id: "profile.reset",
+ cli_path: "radroots profile reset",
+ namespace: "profile",
+ mcp_tool: "profile_reset",
+ rust_request: "ProfileResetRequest",
+ rust_result: "ProfileResetResult",
+ json_kind: "profile.reset",
+ description: "Reset the active runtime profile stores.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::AccountCreate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[2],
+ operation_id: "account.create",
+ cli_path: "radroots account create",
+ namespace: "account",
+ mcp_tool: "account_create",
+ rust_request: "AccountCreateRequest",
+ rust_result: "AccountCreateResult",
+ json_kind: "account.create",
+ description: "Create a local account identity.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::AccountImport,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[3],
+ operation_id: "account.import",
+ cli_path: "radroots account import",
+ namespace: "account",
+ mcp_tool: "account_import",
+ rust_request: "AccountImportRequest",
+ rust_result: "AccountImportResult",
+ json_kind: "account.import",
+ description: "Import an existing account identity.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::AccountSelect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[4],
+ operation_id: "account.select",
+ cli_path: "radroots account select <selector>",
+ namespace: "account",
+ mcp_tool: "account_select",
+ rust_request: "AccountSelectRequest",
+ rust_result: "AccountSelectResult",
+ json_kind: "account.select",
+ description: "Select the active account context.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::AccountList,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[5],
+ operation_id: "account.list",
+ cli_path: "radroots account list",
+ namespace: "account",
+ mcp_tool: "account_list",
+ rust_request: "AccountListRequest",
+ rust_result: "AccountListResult",
+ json_kind: "account.list",
+ description: "List known local accounts.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::AccountRemove,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[6],
+ operation_id: "account.remove",
+ cli_path: "radroots account remove <selector>",
+ namespace: "account",
+ mcp_tool: "account_remove",
+ rust_request: "AccountRemoveRequest",
+ rust_result: "AccountRemoveResult",
+ json_kind: "account.remove",
+ description: "Remove an account from local configuration and storage.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::SignerStatus,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[7],
+ operation_id: "signer.status",
+ cli_path: "radroots signer status",
+ namespace: "signer",
+ mcp_tool: "signer_status",
+ rust_request: "SignerStatusRequest",
+ rust_result: "SignerStatusResult",
+ json_kind: "signer.status",
+ description: "Inspect signer readiness.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::StoreInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[8],
+ operation_id: "store.inspect",
+ cli_path: "radroots store inspect",
+ namespace: "store",
+ mcp_tool: "store_inspect",
+ rust_request: "StoreInspectRequest",
+ rust_result: "StoreInspectResult",
+ json_kind: "store.inspect",
+ description: "Inspect local runtime stores.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::StoreBackup,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[9],
+ operation_id: "store.backup",
+ cli_path: "radroots store backup",
+ namespace: "store",
+ mcp_tool: "store_backup",
+ rust_request: "StoreBackupRequest",
+ rust_result: "StoreBackupResult",
+ json_kind: "store.backup",
+ description: "Create a local runtime-store backup.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::StoreRestore,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[10],
+ operation_id: "store.restore",
+ cli_path: "radroots store restore <source>",
+ namespace: "store",
+ mcp_tool: "store_restore",
+ rust_request: "StoreRestoreRequest",
+ rust_result: "StoreRestoreResult",
+ json_kind: "store.restore",
+ description: "Restore local runtime stores from a backup.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::FarmCreate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[11],
+ operation_id: "farm.create",
+ cli_path: "radroots farm create",
+ namespace: "farm",
+ mcp_tool: "farm_create",
+ rust_request: "FarmCreateRequest",
+ rust_result: "FarmCreateResult",
+ json_kind: "farm.create",
+ description: "Create farm profile data.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::FarmUpdate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[12],
+ operation_id: "farm.update",
+ cli_path: "radroots farm update",
+ namespace: "farm",
+ mcp_tool: "farm_update",
+ rust_request: "FarmUpdateRequest",
+ rust_result: "FarmUpdateResult",
+ json_kind: "farm.update",
+ description: "Update farm profile data.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::FarmPublish,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[13],
+ operation_id: "farm.publish",
+ cli_path: "radroots farm publish",
+ namespace: "farm",
+ mcp_tool: "farm_publish",
+ rust_request: "FarmPublishRequest",
+ rust_result: "FarmPublishResult",
+ json_kind: "farm.publish",
+ description: "Publish farm profile data.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::FarmGet,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[14],
+ operation_id: "farm.get",
+ cli_path: "radroots farm get",
+ namespace: "farm",
+ mcp_tool: "farm_get",
+ rust_request: "FarmGetRequest",
+ rust_result: "FarmGetResult",
+ json_kind: "farm.get",
+ description: "Get farm profile data.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::FarmList,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[15],
+ operation_id: "farm.list",
+ cli_path: "radroots farm list",
+ namespace: "farm",
+ mcp_tool: "farm_list",
+ rust_request: "FarmListRequest",
+ rust_result: "FarmListResult",
+ json_kind: "farm.list",
+ description: "List farm profile data.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingCreate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[16],
+ operation_id: "listing.create",
+ cli_path: "radroots listing create",
+ namespace: "listing",
+ mcp_tool: "listing_create",
+ rust_request: "ListingCreateRequest",
+ rust_result: "ListingCreateResult",
+ json_kind: "listing.create",
+ description: "Create a listing draft.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingUpdate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[17],
+ operation_id: "listing.update",
+ cli_path: "radroots listing update <file>",
+ namespace: "listing",
+ mcp_tool: "listing_update",
+ rust_request: "ListingUpdateRequest",
+ rust_result: "ListingUpdateResult",
+ json_kind: "listing.update",
+ description: "Update a listing.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingPublish,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[18],
+ operation_id: "listing.publish",
+ cli_path: "radroots listing publish <file>",
+ namespace: "listing",
+ mcp_tool: "listing_publish",
+ rust_request: "ListingPublishRequest",
+ rust_result: "ListingPublishResult",
+ json_kind: "listing.publish",
+ description: "Publish a listing.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingPause,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[19],
+ operation_id: "listing.pause",
+ cli_path: "radroots listing pause <file>",
+ namespace: "listing",
+ mcp_tool: "listing_pause",
+ rust_request: "ListingPauseRequest",
+ rust_result: "ListingPauseResult",
+ json_kind: "listing.pause",
+ description: "Pause a listing.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingWithdraw,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[20],
+ operation_id: "listing.withdraw",
+ cli_path: "radroots listing withdraw <file>",
+ namespace: "listing",
+ mcp_tool: "listing_withdraw",
+ rust_request: "ListingWithdrawRequest",
+ rust_result: "ListingWithdrawResult",
+ json_kind: "listing.withdraw",
+ description: "Withdraw a listing.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingGet,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[21],
+ operation_id: "listing.get",
+ cli_path: "radroots listing get <key>",
+ namespace: "listing",
+ mcp_tool: "listing_get",
+ rust_request: "ListingGetRequest",
+ rust_result: "ListingGetResult",
+ json_kind: "listing.get",
+ description: "Get a listing.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ListingList,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[22],
+ operation_id: "listing.list",
+ cli_path: "radroots listing list",
+ namespace: "listing",
+ mcp_tool: "listing_list",
+ rust_request: "ListingListRequest",
+ rust_result: "ListingListResult",
+ json_kind: "listing.list",
+ description: "List listings.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::MarketPull,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[23],
+ operation_id: "market.pull",
+ cli_path: "radroots market pull",
+ namespace: "market",
+ mcp_tool: "market_pull",
+ rust_request: "MarketPullRequest",
+ rust_result: "MarketPullResult",
+ json_kind: "market.pull",
+ description: "Pull market data into the local projection.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::MarketSearch,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[24],
+ operation_id: "market.search",
+ cli_path: "radroots market search <query>",
+ namespace: "market",
+ mcp_tool: "market_search",
+ rust_request: "MarketSearchRequest",
+ rust_result: "MarketSearchResult",
+ json_kind: "market.search",
+ description: "Search local market projections.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::MarketGet,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[25],
+ operation_id: "market.get",
+ cli_path: "radroots market get <key>",
+ namespace: "market",
+ mcp_tool: "market_get",
+ rust_request: "MarketGetRequest",
+ rust_result: "MarketGetResult",
+ json_kind: "market.get",
+ description: "Get market listing details.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketCreate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[26],
+ operation_id: "basket.create",
+ cli_path: "radroots basket create",
+ namespace: "basket",
+ mcp_tool: "basket_create",
+ rust_request: "BasketCreateRequest",
+ rust_result: "BasketCreateResult",
+ json_kind: "basket.create",
+ description: "Create a basket.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketGet,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[27],
+ operation_id: "basket.get",
+ cli_path: "radroots basket get <basket-id>",
+ namespace: "basket",
+ mcp_tool: "basket_get",
+ rust_request: "BasketGetRequest",
+ rust_result: "BasketGetResult",
+ json_kind: "basket.get",
+ description: "Get a basket.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketList,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[28],
+ operation_id: "basket.list",
+ cli_path: "radroots basket list",
+ namespace: "basket",
+ mcp_tool: "basket_list",
+ rust_request: "BasketListRequest",
+ rust_result: "BasketListResult",
+ json_kind: "basket.list",
+ description: "List baskets.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketItemAdd,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[29],
+ operation_id: "basket.item.add",
+ cli_path: "radroots basket item add <basket-id>",
+ namespace: "basket",
+ mcp_tool: "basket_item_add",
+ rust_request: "BasketItemAddRequest",
+ rust_result: "BasketItemAddResult",
+ json_kind: "basket.item.add",
+ description: "Add an item to a basket.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketItemUpdate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[30],
+ operation_id: "basket.item.update",
+ cli_path: "radroots basket item update <basket-id>",
+ namespace: "basket",
+ mcp_tool: "basket_item_update",
+ rust_request: "BasketItemUpdateRequest",
+ rust_result: "BasketItemUpdateResult",
+ json_kind: "basket.item.update",
+ description: "Update a basket item.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketItemRemove,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[31],
+ operation_id: "basket.item.remove",
+ cli_path: "radroots basket item remove <basket-id>",
+ namespace: "basket",
+ mcp_tool: "basket_item_remove",
+ rust_request: "BasketItemRemoveRequest",
+ rust_result: "BasketItemRemoveResult",
+ json_kind: "basket.item.remove",
+ description: "Remove a basket item.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::BasketQuote,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[32],
+ operation_id: "basket.quote",
+ cli_path: "radroots basket quote <basket-id>",
+ namespace: "basket",
+ mcp_tool: "basket_quote",
+ rust_request: "BasketQuoteRequest",
+ rust_result: "BasketQuoteResult",
+ json_kind: "basket.quote",
+ description: "Quote a basket.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TradeRequest,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[33],
+ operation_id: "trade.request",
+ cli_path: "radroots trade request",
+ namespace: "trade",
+ mcp_tool: "trade_request",
+ rust_request: "TradeRequestRequest",
+ rust_result: "TradeRequestResult",
+ json_kind: "trade.request",
+ description: "Request a trade from a quoted basket.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TradeGet,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[34],
+ operation_id: "trade.get",
+ cli_path: "radroots trade get <key>",
+ namespace: "trade",
+ mcp_tool: "trade_get",
+ rust_request: "TradeGetRequest",
+ rust_result: "TradeGetResult",
+ json_kind: "trade.get",
+ description: "Get trade details.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TradeList,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[35],
+ operation_id: "trade.list",
+ cli_path: "radroots trade list",
+ namespace: "trade",
+ mcp_tool: "trade_list",
+ rust_request: "TradeListRequest",
+ rust_result: "TradeListResult",
+ json_kind: "trade.list",
+ description: "List trades.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TradeAccept,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[36],
+ operation_id: "trade.accept",
+ cli_path: "radroots trade accept <key>",
+ namespace: "trade",
+ mcp_tool: "trade_accept",
+ rust_request: "TradeAcceptRequest",
+ rust_result: "TradeAcceptResult",
+ json_kind: "trade.accept",
+ description: "Accept a trade request.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TradeDecline,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[37],
+ operation_id: "trade.decline",
+ cli_path: "radroots trade decline <key>",
+ namespace: "trade",
+ mcp_tool: "trade_decline",
+ rust_request: "TradeDeclineRequest",
+ rust_result: "TradeDeclineResult",
+ json_kind: "trade.decline",
+ description: "Decline a trade request.",
+ role: OperationRole::Seller,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TradeCancel,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[38],
+ operation_id: "trade.cancel",
+ cli_path: "radroots trade cancel <key>",
+ namespace: "trade",
+ mcp_tool: "trade_cancel",
+ rust_request: "TradeCancelRequest",
+ rust_result: "TradeCancelResult",
+ json_kind: "trade.cancel",
+ description: "Cancel a trade request.",
+ role: OperationRole::Buyer,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ValidationStatus,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[39],
+ operation_id: "validation.status",
+ cli_path: "radroots validation status",
+ namespace: "validation",
+ mcp_tool: "validation_status",
+ rust_request: "ValidationStatusRequest",
+ rust_result: "ValidationStatusResult",
+ json_kind: "validation.status",
+ description: "Inspect validation status.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ValidationReceiptGet,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[40],
+ operation_id: "validation.receipt.get",
+ cli_path: "radroots validation receipt get <receipt-event-id>",
+ namespace: "validation",
+ mcp_tool: "validation_receipt_get",
+ rust_request: "ValidationReceiptGetRequest",
+ rust_result: "ValidationReceiptGetResult",
+ json_kind: "validation.receipt.get",
+ description: "Get a validation receipt.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::ValidationReceiptVerify,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[41],
+ operation_id: "validation.receipt.verify",
+ cli_path: "radroots validation receipt verify <receipt-event-id>",
+ namespace: "validation",
+ mcp_tool: "validation_receipt_verify",
+ rust_request: "ValidationReceiptVerifyRequest",
+ rust_result: "ValidationReceiptVerifyResult",
+ json_kind: "validation.receipt.verify",
+ description: "Verify a validation receipt.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::SyncStatus,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[42],
+ operation_id: "sync.status",
+ cli_path: "radroots sync status",
+ namespace: "sync",
+ mcp_tool: "sync_status",
+ rust_request: "SyncStatusRequest",
+ rust_result: "SyncStatusResult",
+ json_kind: "sync.status",
+ description: "Inspect synchronization status.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::SyncPull,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[43],
+ operation_id: "sync.pull",
+ cli_path: "radroots sync pull",
+ namespace: "sync",
+ mcp_tool: "sync_pull",
+ rust_request: "SyncPullRequest",
+ rust_result: "SyncPullResult",
+ json_kind: "sync.pull",
+ description: "Pull synchronization events.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::SyncPush,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[44],
+ operation_id: "sync.push",
+ cli_path: "radroots sync push",
+ namespace: "sync",
+ mcp_tool: "sync_push",
+ rust_request: "SyncPushRequest",
+ rust_result: "SyncPushResult",
+ json_kind: "sync.push",
+ description: "Push queued synchronization events.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::HealthInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[45],
+ operation_id: "health.inspect",
+ cli_path: "radroots health inspect",
+ namespace: "health",
+ mcp_tool: "health_inspect",
+ rust_request: "HealthInspectRequest",
+ rust_result: "HealthInspectResult",
+ json_kind: "health.inspect",
+ description: "Inspect runtime health.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TransportCapabilityList,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[46],
+ operation_id: "transport.capability.list",
+ cli_path: "radroots transport capability list",
+ namespace: "transport",
+ mcp_tool: "transport_capability_list",
+ rust_request: "TransportCapabilityListRequest",
+ rust_result: "TransportCapabilityListResult",
+ json_kind: "transport.capability.list",
+ description: "List transport capabilities.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TransportConfigInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[47],
+ operation_id: "transport.config.inspect",
+ cli_path: "radroots transport config inspect",
+ namespace: "transport",
+ mcp_tool: "transport_config_inspect",
+ rust_request: "TransportConfigInspectRequest",
+ rust_result: "TransportConfigInspectResult",
+ json_kind: "transport.config.inspect",
+ description: "Inspect transport configuration.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TransportConfigUpdate,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[48],
+ operation_id: "transport.config.update",
+ cli_path: "radroots transport config update --kind local-only",
+ namespace: "transport",
+ mcp_tool: "transport_config_update",
+ rust_request: "TransportConfigUpdateRequest",
+ rust_result: "TransportConfigUpdateResult",
+ json_kind: "transport.config.update",
+ description: "Update transport configuration.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TransportStatusInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[49],
+ operation_id: "transport.status.inspect",
+ cli_path: "radroots transport status inspect",
+ namespace: "transport",
+ mcp_tool: "transport_status_inspect",
+ rust_request: "TransportStatusInspectRequest",
+ rust_result: "TransportStatusInspectResult",
+ json_kind: "transport.status.inspect",
+ description: "Inspect transport status.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TransportDeliveryInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[50],
+ operation_id: "transport.delivery.inspect",
+ cli_path: "radroots transport delivery inspect",
+ namespace: "transport",
+ mcp_tool: "transport_delivery_inspect",
+ rust_request: "TransportDeliveryInspectRequest",
+ rust_result: "TransportDeliveryInspectResult",
+ json_kind: "transport.delivery.inspect",
+ description: "Inspect transport delivery.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::TransportDeliveryRetry,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[51],
+ operation_id: "transport.delivery.retry",
+ cli_path: "radroots transport delivery retry",
+ namespace: "transport",
+ mcp_tool: "transport_delivery_retry",
+ rust_request: "TransportDeliveryRetryRequest",
+ rust_result: "TransportDeliveryRetryResult",
+ json_kind: "transport.delivery.retry",
+ description: "Retry transport delivery.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: true,
+ },
+ OperationSpec {
+ runtime_operation_id: RuntimeOperationIdV1::DiagnosticsInspect,
+ descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[52],
+ operation_id: "diagnostics.inspect",
+ cli_path: "radroots diagnostics inspect",
+ namespace: "diagnostics",
+ mcp_tool: "diagnostics_inspect",
+ rust_request: "DiagnosticsInspectRequest",
+ rust_result: "DiagnosticsInspectResult",
+ json_kind: "diagnostics.inspect",
+ description: "Inspect runtime diagnostics.",
+ role: OperationRole::Any,
+ supports_json: true,
+ supports_ndjson: false,
+ },
+];
diff --git a/src/generated/runtime_contract_target.rs b/src/generated/runtime_contract_target.rs
@@ -0,0 +1,57 @@
+// @generated by radroots_sdk_xtask. Do not edit by hand.
+
+target_operation_contracts! {
+ ProfileInspect => (ProfileInspectRequest, ProfileInspectResult, "profile.inspect"),
+ ProfileReset => (ProfileResetRequest, ProfileResetResult, "profile.reset"),
+ AccountCreate => (AccountCreateRequest, AccountCreateResult, "account.create"),
+ AccountImport => (AccountImportRequest, AccountImportResult, "account.import"),
+ AccountSelect => (AccountSelectRequest, AccountSelectResult, "account.select"),
+ AccountList => (AccountListRequest, AccountListResult, "account.list"),
+ AccountRemove => (AccountRemoveRequest, AccountRemoveResult, "account.remove"),
+ SignerStatus => (SignerStatusRequest, SignerStatusResult, "signer.status"),
+ StoreInspect => (StoreInspectRequest, StoreInspectResult, "store.inspect"),
+ StoreBackup => (StoreBackupRequest, StoreBackupResult, "store.backup"),
+ StoreRestore => (StoreRestoreRequest, StoreRestoreResult, "store.restore"),
+ FarmCreate => (FarmCreateRequest, FarmCreateResult, "farm.create"),
+ FarmUpdate => (FarmUpdateRequest, FarmUpdateResult, "farm.update"),
+ FarmPublish => (FarmPublishRequest, FarmPublishResult, "farm.publish"),
+ FarmGet => (FarmGetRequest, FarmGetResult, "farm.get"),
+ FarmList => (FarmListRequest, FarmListResult, "farm.list"),
+ ListingCreate => (ListingCreateRequest, ListingCreateResult, "listing.create"),
+ ListingUpdate => (ListingUpdateRequest, ListingUpdateResult, "listing.update"),
+ ListingPublish => (ListingPublishRequest, ListingPublishResult, "listing.publish"),
+ ListingPause => (ListingPauseRequest, ListingPauseResult, "listing.pause"),
+ ListingWithdraw => (ListingWithdrawRequest, ListingWithdrawResult, "listing.withdraw"),
+ ListingGet => (ListingGetRequest, ListingGetResult, "listing.get"),
+ ListingList => (ListingListRequest, ListingListResult, "listing.list"),
+ MarketPull => (MarketPullRequest, MarketPullResult, "market.pull"),
+ MarketSearch => (MarketSearchRequest, MarketSearchResult, "market.search"),
+ MarketGet => (MarketGetRequest, MarketGetResult, "market.get"),
+ BasketCreate => (BasketCreateRequest, BasketCreateResult, "basket.create"),
+ BasketGet => (BasketGetRequest, BasketGetResult, "basket.get"),
+ BasketList => (BasketListRequest, BasketListResult, "basket.list"),
+ BasketItemAdd => (BasketItemAddRequest, BasketItemAddResult, "basket.item.add"),
+ BasketItemUpdate => (BasketItemUpdateRequest, BasketItemUpdateResult, "basket.item.update"),
+ BasketItemRemove => (BasketItemRemoveRequest, BasketItemRemoveResult, "basket.item.remove"),
+ BasketQuote => (BasketQuoteRequest, BasketQuoteResult, "basket.quote"),
+ TradeRequest => (TradeRequestRequest, TradeRequestResult, "trade.request"),
+ TradeGet => (TradeGetRequest, TradeGetResult, "trade.get"),
+ TradeList => (TradeListRequest, TradeListResult, "trade.list"),
+ TradeAccept => (TradeAcceptRequest, TradeAcceptResult, "trade.accept"),
+ TradeDecline => (TradeDeclineRequest, TradeDeclineResult, "trade.decline"),
+ TradeCancel => (TradeCancelRequest, TradeCancelResult, "trade.cancel"),
+ ValidationStatus => (ValidationStatusRequest, ValidationStatusResult, "validation.status"),
+ ValidationReceiptGet => (ValidationReceiptGetRequest, ValidationReceiptGetResult, "validation.receipt.get"),
+ ValidationReceiptVerify => (ValidationReceiptVerifyRequest, ValidationReceiptVerifyResult, "validation.receipt.verify"),
+ SyncStatus => (SyncStatusRequest, SyncStatusResult, "sync.status"),
+ SyncPull => (SyncPullRequest, SyncPullResult, "sync.pull"),
+ SyncPush => (SyncPushRequest, SyncPushResult, "sync.push"),
+ HealthInspect => (HealthInspectRequest, HealthInspectResult, "health.inspect"),
+ TransportCapabilityList => (TransportCapabilityListRequest, TransportCapabilityListResult, "transport.capability.list"),
+ TransportConfigInspect => (TransportConfigInspectRequest, TransportConfigInspectResult, "transport.config.inspect"),
+ TransportConfigUpdate => (TransportConfigUpdateRequest, TransportConfigUpdateResult, "transport.config.update"),
+ TransportStatusInspect => (TransportStatusInspectRequest, TransportStatusInspectResult, "transport.status.inspect"),
+ TransportDeliveryInspect => (TransportDeliveryInspectRequest, TransportDeliveryInspectResult, "transport.delivery.inspect"),
+ TransportDeliveryRetry => (TransportDeliveryRetryRequest, TransportDeliveryRetryResult, "transport.delivery.retry"),
+ DiagnosticsInspect => (DiagnosticsInspectRequest, DiagnosticsInspectResult, "diagnostics.inspect"),
+}
diff --git a/src/main.rs b/src/main.rs
@@ -7,6 +7,10 @@ use std::time::{SystemTime, UNIX_EPOCH};
use clap::Parser;
use radroots_cli::{ops, registry, runtime};
+use radroots_runtime_contract_v1::{
+ ExecutionModeV1, RUNTIME_CONTRACT_NAME_V1, RUNTIME_CONTRACT_VERSION_V1, RuntimeContractErrorV1,
+ RuntimeRequestEnvelopeV1,
+};
use serde_json::json;
use radroots_cli::cli::input::runtime_invocation_args_from_target;
@@ -29,7 +33,7 @@ use radroots_cli::out::terminal::renderer::{
TerminalRenderContext, TerminalVerbosity, render_terminal_document,
};
use radroots_cli::registry::{
- ApprovalPolicy, NetworkRequirement, OPERATION_REGISTRY, network_requirement,
+ NetworkRequirement, OPERATION_REGISTRY, network_requirement,
requires_delivery_capable_transport_profile, requires_local_signer_mode,
};
use radroots_cli::runtime::config::{
@@ -65,7 +69,7 @@ fn run() -> Result<ExitCode, runtime::RuntimeError> {
let config = match RuntimeConfig::from_system(&runtime_invocation_args_from_target(&args)) {
Ok(config) => config,
Err(error) => {
- let envelope = runtime_config_failure_envelope(&request, error.into());
+ let envelope = runtime_config_failure_envelope(&request, error);
return render_envelope(&envelope, &pre_runtime_render_config);
}
};
@@ -91,19 +95,10 @@ fn execute_request(
logging: &runtime::logging::LoggingState,
) -> OutputEnvelope {
match request {
- TargetOperationRequest::WorkspaceInit(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::WorkspaceGet(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::HealthStatusGet(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::HealthCheckRun(request) => {
+ TargetOperationRequest::ProfileReset(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::ConfigGet(request) => {
+ TargetOperationRequest::ProfileInspect(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
TargetOperationRequest::AccountCreate(request) => {
@@ -112,73 +107,46 @@ fn execute_request(
TargetOperationRequest::AccountImport(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::AccountAttachSecret(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountGet(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
TargetOperationRequest::AccountList(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
TargetOperationRequest::AccountRemove(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::AccountSelectionGet(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountSelectionUpdate(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountSelectionClear(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::StoreInit(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::StoreStatusGet(request) => {
+ TargetOperationRequest::AccountSelect(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::StoreExport(request) => {
+ TargetOperationRequest::StoreInspect(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::StoreBackupCreate(request) => {
+ TargetOperationRequest::StoreBackup(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::StoreBackupRestore(request) => {
+ TargetOperationRequest::StoreRestore(request) => {
execute_with(CoreOperationService::new(config, logging), request)
}
- TargetOperationRequest::SignerStatusGet(request) => {
+ TargetOperationRequest::SignerStatus(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::TransportProfileGet(request) => {
+ TargetOperationRequest::TransportCapabilityList(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::TransportProfileSet(request) => {
+ TargetOperationRequest::TransportConfigInspect(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::TransportStatus(request) => {
+ TargetOperationRequest::TransportConfigUpdate(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::TransportOutboxStatus(request) => {
+ TargetOperationRequest::TransportStatusInspect(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::TransportOutboxPush(request) => {
+ TargetOperationRequest::TransportDeliveryInspect(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::MeshScopeGet(request) => {
+ TargetOperationRequest::TransportDeliveryRetry(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::MeshScopeSet(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::MeshStatus(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::MeshPolicyCheck(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::SyncStatusGet(request) => {
+ TargetOperationRequest::SyncStatus(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
TargetOperationRequest::SyncPull(request) => {
@@ -187,7 +155,7 @@ fn execute_request(
TargetOperationRequest::SyncPush(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::SyncWatch(request) => {
+ TargetOperationRequest::DiagnosticsInspect(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
TargetOperationRequest::FarmCreate(request) => {
@@ -196,25 +164,10 @@ fn execute_request(
TargetOperationRequest::FarmGet(request) => {
execute_with(FarmOperationService::new(config), request)
}
- TargetOperationRequest::FarmRebind(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmProfileUpdate(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmLocationSet(request) => {
+ TargetOperationRequest::FarmUpdate(request) => {
execute_with(FarmOperationService::new(config), request)
}
- TargetOperationRequest::FarmLocationGet(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmLocationClear(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmFulfillmentUpdate(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmReadinessCheck(request) => {
+ TargetOperationRequest::FarmList(request) => {
execute_with(FarmOperationService::new(config), request)
}
TargetOperationRequest::FarmPublish(request) => {
@@ -229,34 +182,25 @@ fn execute_request(
TargetOperationRequest::ListingList(request) => {
execute_with(ListingOperationService::new(config), request)
}
- TargetOperationRequest::ListingAppList(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingAppExport(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
TargetOperationRequest::ListingUpdate(request) => {
execute_with(ListingOperationService::new(config), request)
}
- TargetOperationRequest::ListingValidate(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingRebind(request) => {
+ TargetOperationRequest::ListingPublish(request) => {
execute_with(ListingOperationService::new(config), request)
}
- TargetOperationRequest::ListingPublish(request) => {
+ TargetOperationRequest::ListingPause(request) => {
execute_with(ListingOperationService::new(config), request)
}
- TargetOperationRequest::ListingArchive(request) => {
+ TargetOperationRequest::ListingWithdraw(request) => {
execute_with(ListingOperationService::new(config), request)
}
- TargetOperationRequest::MarketRefresh(request) => {
+ TargetOperationRequest::MarketPull(request) => {
execute_with(MarketOperationService::new(config), request)
}
- TargetOperationRequest::MarketProductSearch(request) => {
+ TargetOperationRequest::MarketSearch(request) => {
execute_with(MarketOperationService::new(config), request)
}
- TargetOperationRequest::MarketListingGet(request) => {
+ TargetOperationRequest::MarketGet(request) => {
execute_with(MarketOperationService::new(config), request)
}
TargetOperationRequest::BasketCreate(request) => {
@@ -277,19 +221,10 @@ fn execute_request(
TargetOperationRequest::BasketItemRemove(request) => {
execute_with(BasketOperationService::new(config), request)
}
- TargetOperationRequest::BasketAdjustmentAdd(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketAdjustmentRemove(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketValidate(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketQuoteCreate(request) => {
+ TargetOperationRequest::BasketQuote(request) => {
execute_with(BasketOperationService::new(config), request)
}
- TargetOperationRequest::TradeSubmit(request) => {
+ TargetOperationRequest::TradeRequest(request) => {
execute_with(TradeOperationService::new(config), request)
}
TargetOperationRequest::TradeGet(request) => {
@@ -298,15 +233,6 @@ fn execute_request(
TargetOperationRequest::TradeList(request) => {
execute_with(TradeOperationService::new(config), request)
}
- TargetOperationRequest::TradeAppList(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeAppExport(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeRebind(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
TargetOperationRequest::TradeAccept(request) => {
execute_with(TradeOperationService::new(config), request)
}
@@ -316,24 +242,18 @@ fn execute_request(
TargetOperationRequest::TradeCancel(request) => {
execute_with(TradeOperationService::new(config), request)
}
- TargetOperationRequest::TradeStatusGet(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeEventList(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeEventWatch(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::ValidationReceiptGet(request) => {
+ TargetOperationRequest::ValidationStatus(request) => {
execute_with(ValidationOperationService::new(config), request)
}
- TargetOperationRequest::ValidationReceiptList(request) => {
+ TargetOperationRequest::ValidationReceiptGet(request) => {
execute_with(ValidationOperationService::new(config), request)
}
TargetOperationRequest::ValidationReceiptVerify(request) => {
execute_with(ValidationOperationService::new(config), request)
}
+ TargetOperationRequest::HealthInspect(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
}
}
@@ -383,15 +303,52 @@ fn validate_pre_runtime_request_contract(
message: format!("`{}` does not support --format ndjson", spec.cli_path),
});
}
- if request.context().dry_run && !spec.supports_dry_run {
+ if request.context().dry_run && !spec.supports_dry_run() {
return Err(OperationAdapterError::InvalidInput {
operation_id: spec.operation_id.to_owned(),
message: format!("`{}` does not support --dry-run", spec.cli_path),
});
}
+ validate_idempotency_contract(request)?;
+ if !request.context().dry_run
+ && spec.requires_approval()
+ && !request.context().has_operator_approval()
+ {
+ return Err(OperationAdapterError::approval_required(spec.operation_id));
+ }
Ok(())
}
+fn validate_idempotency_contract(
+ request: &TargetOperationRequest,
+) -> Result<(), OperationAdapterError> {
+ let spec = request.spec();
+ RuntimeRequestEnvelopeV1 {
+ contract: RUNTIME_CONTRACT_NAME_V1.to_owned(),
+ contract_version: RUNTIME_CONTRACT_VERSION_V1,
+ operation_id: spec.runtime_operation_id,
+ operation_schema_version: spec.descriptor.schema_version,
+ request_id: "cli-pre-runtime-contract-validation".to_owned(),
+ actor_pubkey: None,
+ idempotency_key: request.context().idempotency_key.clone(),
+ approval: request.context().approval_proof.clone(),
+ execution_mode: ExecutionModeV1::Embedded,
+ request_json: "{}".to_owned(),
+ }
+ .validate()
+ .map_err(|error| idempotency_contract_error(spec.operation_id, error))
+}
+
+fn idempotency_contract_error(
+ operation_id: &str,
+ error: RuntimeContractErrorV1,
+) -> OperationAdapterError {
+ OperationAdapterError::InvalidInput {
+ operation_id: operation_id.to_owned(),
+ message: error.to_string(),
+ }
+}
+
fn validate_signer_mode_contract(
request: &TargetOperationRequest,
config: &RuntimeConfig,
@@ -488,8 +445,6 @@ fn validate_transport_profile_contract(
request.context().network_mode,
OperationNetworkMode::Offline
)
- || (spec.approval_policy == ApprovalPolicy::Required
- && request.context().requires_approval_token())
{
return Ok(());
}
@@ -779,12 +734,12 @@ mod tests {
#[test]
fn terminal_registry_failure_envelope_is_structured_internal_error() {
let original = OutputEnvelope::success(
- "workspace.get",
+ "profile.inspect",
Value::Null,
EnvelopeContext::new("req_test", false),
);
let error = TerminalRendererRegistry::new()
- .validate_operation_ids(["workspace.get"])
+ .validate_operation_ids(["profile.inspect"])
.expect_err("missing renderer should fail");
let failure = terminal_registry_failure_envelope(&original, error);
@@ -800,8 +755,9 @@ mod tests {
);
assert!(rendered.starts_with("✕ Command failed\n"));
assert!(
- rendered
- .contains("Reason terminal renderer registry invariant failed for workspace.get")
+ rendered.contains(
+ "Reason terminal renderer registry invariant failed for profile.inspect"
+ )
);
assert!(!rendered.contains("RuntimeError::Config"));
}
diff --git a/src/ops/context.rs b/src/ops/context.rs
@@ -1,20 +1,20 @@
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use radroots_runtime_contract_v1::{ApprovalProofV1, RuntimeContractErrorV1};
+
use crate::cli::{TargetCliArgs, TargetOutputFormat};
+use crate::ops::OperationAdapterError;
use crate::out::envelope::{EnvelopeActor, EnvelopeContext, OutputFormat as EnvelopeOutputFormat};
use crate::runtime::config::OutputFormat as RuntimeOutputFormat;
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OperationOutputFormat {
+ #[default]
Terminal,
Json,
Ndjson,
}
-impl Default for OperationOutputFormat {
- fn default() -> Self {
- Self::Terminal
- }
-}
-
impl From<TargetOutputFormat> for OperationOutputFormat {
fn from(format: TargetOutputFormat) -> Self {
match format {
@@ -35,31 +35,21 @@ impl From<RuntimeOutputFormat> for OperationOutputFormat {
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OperationNetworkMode {
+ #[default]
Default,
Offline,
Online,
}
-impl Default for OperationNetworkMode {
- fn default() -> Self {
- Self::Default
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OperationInputMode {
+ #[default]
PromptingAllowed,
NoInput,
}
-impl Default for OperationInputMode {
- fn default() -> Self {
- Self::PromptingAllowed
- }
-}
-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct OperationContext {
pub output_format: OperationOutputFormat,
@@ -68,7 +58,8 @@ pub struct OperationContext {
pub dry_run: bool,
pub idempotency_key: Option<String>,
pub correlation_id: Option<String>,
- pub approval_token: Option<String>,
+ pub approval_proof: Option<ApprovalProofV1>,
+ pub yes: bool,
pub input_mode: OperationInputMode,
pub quiet: bool,
pub verbose: bool,
@@ -76,8 +67,11 @@ pub struct OperationContext {
}
impl OperationContext {
- pub fn from_target_args(args: &TargetCliArgs) -> Self {
- Self {
+ pub fn from_target_args(
+ args: &TargetCliArgs,
+ operation_id: &'static str,
+ ) -> Result<Self, OperationAdapterError> {
+ Ok(Self {
output_format: args
.format
.map(OperationOutputFormat::from)
@@ -93,7 +87,8 @@ impl OperationContext {
dry_run: args.dry_run,
idempotency_key: args.idempotency_key.clone(),
correlation_id: args.correlation_id.clone(),
- approval_token: args.approval_token.clone(),
+ approval_proof: parse_approval_proof(args.approval_proof.as_deref(), operation_id)?,
+ yes: args.yes,
input_mode: if args.no_input {
OperationInputMode::NoInput
} else {
@@ -102,7 +97,7 @@ impl OperationContext {
quiet: args.quiet,
verbose: args.verbose,
trace: args.trace,
- }
+ })
}
pub fn envelope_context(&self, request_id: impl Into<String>) -> EnvelopeContext {
@@ -121,13 +116,94 @@ impl OperationContext {
context
}
- pub fn requires_approval_token(&self) -> bool {
- !self.dry_run && !self.has_approval_token()
+ pub fn has_operator_approval(&self) -> bool {
+ self.yes || self.approval_proof.is_some()
+ }
+}
+
+fn parse_approval_proof(
+ value: Option<&str>,
+ operation_id: &'static str,
+) -> Result<Option<ApprovalProofV1>, OperationAdapterError> {
+ let Some(value) = value.map(str::trim) else {
+ return Ok(None);
+ };
+ if value.is_empty() {
+ return Err(invalid_approval(
+ operation_id,
+ "approval proof JSON must not be empty",
+ ));
}
+ let proof = serde_json::from_str::<ApprovalProofV1>(value).map_err(|error| {
+ invalid_approval(
+ operation_id,
+ format!("approval proof must be valid ApprovalProofV1 JSON: {error}"),
+ )
+ })?;
+ validate_approval_proof(&proof, operation_id)?;
+ Ok(Some(proof))
+}
- pub fn has_approval_token(&self) -> bool {
- self.approval_token
- .as_deref()
- .is_some_and(|token| !token.trim().is_empty())
+fn validate_approval_proof(
+ proof: &ApprovalProofV1,
+ operation_id: &'static str,
+) -> Result<(), OperationAdapterError> {
+ if proof.operation_id.as_str() != operation_id {
+ return Err(invalid_approval(
+ operation_id,
+ format!(
+ "approval proof operation `{}` does not match request operation `{operation_id}`",
+ proof.operation_id.as_str()
+ ),
+ ));
}
+ proof
+ .request_digest
+ .validate()
+ .map_err(|error| invalid_approval(operation_id, runtime_contract_error(error)))?;
+ for (field, value) in [
+ ("proof_id", proof.proof_id.as_str()),
+ ("signer_pubkey", proof.signer_pubkey.as_str()),
+ ("signature", proof.signature.as_str()),
+ ] {
+ if value.trim().is_empty() {
+ return Err(invalid_approval(
+ operation_id,
+ format!("approval proof `{field}` must not be empty"),
+ ));
+ }
+ }
+ if proof.signed_at_unix_ms == 0 {
+ return Err(invalid_approval(
+ operation_id,
+ "approval proof `signed_at_unix_ms` must be nonzero",
+ ));
+ }
+ if let Some(expires_at_unix_ms) = proof.expires_at_unix_ms {
+ let now_unix_ms = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis() as u64)
+ .unwrap_or_default();
+ if expires_at_unix_ms <= now_unix_ms {
+ return Err(OperationAdapterError::ApprovalRequired {
+ operation_id: operation_id.to_owned(),
+ message: "approval proof has expired".to_owned(),
+ });
+ }
+ }
+ Ok(())
+}
+
+fn invalid_approval(
+ operation_id: &'static str,
+ message: impl Into<String>,
+) -> OperationAdapterError {
+ OperationAdapterError::InvalidInput {
+ operation_id: operation_id.to_owned(),
+ message: message.into(),
+ }
+}
+
+fn runtime_contract_error(error: RuntimeContractErrorV1) -> String {
+ error.to_string()
}
diff --git a/src/ops/error.rs b/src/ops/error.rs
@@ -111,24 +111,45 @@ pub enum OperationAdapterError {
operation_id: String,
message: String,
},
- #[error("operation `{operation_id}` failed: {message}")]
- DetailedFailure {
+ #[error("operation `{}` failed: {}", .0.operation_id, .0.message)]
+ DetailedFailure(Box<OperationDetailedFailure>),
+ #[error("operation runtime error: {0}")]
+ Runtime(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct OperationDetailedFailure {
+ pub operation_id: String,
+ pub code: String,
+ pub class: String,
+ pub message: String,
+ pub exit_code: CliExitCode,
+ pub detail_json: String,
+}
+
+impl OperationAdapterError {
+ fn detailed_failure(
operation_id: String,
code: String,
class: String,
message: String,
exit_code: CliExitCode,
detail_json: String,
- },
- #[error("operation runtime error: {0}")]
- Runtime(String),
-}
+ ) -> Self {
+ Self::DetailedFailure(Box::new(OperationDetailedFailure {
+ operation_id,
+ code,
+ class,
+ message,
+ exit_code,
+ detail_json,
+ }))
+ }
-impl OperationAdapterError {
pub fn approval_required(operation_id: &str) -> Self {
Self::ApprovalRequired {
operation_id: operation_id.to_owned(),
- message: "missing required `approval_token` input".to_owned(),
+ message: "missing required V1 approval proof or --yes confirmation".to_owned(),
}
}
@@ -170,25 +191,25 @@ impl OperationAdapterError {
message: String,
detail: Value,
) -> Self {
- Self::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: "operation_unavailable".to_owned(),
- class: "operation".to_owned(),
+ Self::detailed_failure(
+ operation_id.to_owned(),
+ "operation_unavailable".to_owned(),
+ "operation".to_owned(),
message,
- exit_code: CliExitCode::RuntimeUnavailable,
- detail_json: detail.to_string(),
- }
+ CliExitCode::RuntimeUnavailable,
+ detail.to_string(),
+ )
}
pub fn not_found_with_detail(operation_id: &str, message: String, detail: Value) -> Self {
- Self::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: "not_found".to_owned(),
- class: "resource".to_owned(),
+ Self::detailed_failure(
+ operation_id.to_owned(),
+ "not_found".to_owned(),
+ "resource".to_owned(),
message,
- exit_code: CliExitCode::NotFound,
- detail_json: detail.to_string(),
- }
+ CliExitCode::NotFound,
+ detail.to_string(),
+ )
}
pub fn not_implemented(operation_id: &str, message: String) -> Self {
@@ -199,14 +220,14 @@ impl OperationAdapterError {
}
pub fn not_implemented_with_detail(operation_id: &str, message: String, detail: Value) -> Self {
- Self::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: "not_implemented".to_owned(),
- class: "operation".to_owned(),
+ Self::detailed_failure(
+ operation_id.to_owned(),
+ "not_implemented".to_owned(),
+ "operation".to_owned(),
message,
- exit_code: CliExitCode::RuntimeUnavailable,
- detail_json: detail.to_string(),
- }
+ CliExitCode::RuntimeUnavailable,
+ detail.to_string(),
+ )
}
pub fn network_unavailable_with_detail(
@@ -214,14 +235,14 @@ impl OperationAdapterError {
message: String,
detail: Value,
) -> Self {
- Self::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: "network_unavailable".to_owned(),
- class: "network".to_owned(),
+ Self::detailed_failure(
+ operation_id.to_owned(),
+ "network_unavailable".to_owned(),
+ "network".to_owned(),
message,
- exit_code: CliExitCode::SyncOrNetworkFailure,
- detail_json: detail.to_string(),
- }
+ CliExitCode::SyncOrNetworkFailure,
+ detail.to_string(),
+ )
}
pub fn validation_failed_with_detail(
@@ -229,14 +250,14 @@ impl OperationAdapterError {
message: String,
detail: Value,
) -> Self {
- Self::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: "validation_failed".to_owned(),
- class: "validation".to_owned(),
+ Self::detailed_failure(
+ operation_id.to_owned(),
+ "validation_failed".to_owned(),
+ "validation".to_owned(),
message,
- exit_code: CliExitCode::ValidationFailed,
- detail_json: detail.to_string(),
- }
+ CliExitCode::ValidationFailed,
+ detail.to_string(),
+ )
}
pub fn unavailable(operation_id: &str, message: String) -> Self {
@@ -349,14 +370,14 @@ impl OperationAdapterError {
Value::Array(actions.into_iter().map(Value::String).collect()),
);
}
- Self::DetailedFailure {
- operation_id: operation_id.to_owned(),
+ Self::detailed_failure(
+ operation_id.to_owned(),
code,
class,
message,
exit_code,
- detail_json: detail.to_string(),
- }
+ detail.to_string(),
+ )
}
pub fn to_output_error(&self) -> OutputError {
@@ -509,20 +530,13 @@ impl OperationAdapterError {
message,
CliExitCode::RuntimeUnavailable,
),
- Self::DetailedFailure {
- operation_id,
- code,
- class,
- message,
- exit_code,
- detail_json,
- } => runtime_output_error_with_detail(
- code.as_str(),
- operation_id,
- class,
- message,
- *exit_code,
- detail_json,
+ Self::DetailedFailure(failure) => runtime_output_error_with_detail(
+ failure.code.as_str(),
+ failure.operation_id.as_str(),
+ failure.class.as_str(),
+ failure.message.as_str(),
+ failure.exit_code,
+ failure.detail_json.as_str(),
),
Self::UnknownOperation(operation_id) => OutputError::new(
"unknown_operation",
@@ -582,10 +596,10 @@ fn sdk_recovery_next_actions(
RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey
| RadrootsSdkRecoveryAction::FixRequest => Some(operation_retry_action(operation_id)),
RadrootsSdkRecoveryAction::InspectLocalStores => {
- Some("radroots store status get".to_owned())
+ Some("radroots store inspect".to_owned())
}
RadrootsSdkRecoveryAction::ConfigureTransportTargets => {
- Some("radroots transport profile get".to_owned())
+ Some("radroots transport config inspect".to_owned())
}
RadrootsSdkRecoveryAction::SelectAuthorizedActor => {
Some("radroots account list".to_owned())
@@ -594,7 +608,7 @@ fn sdk_recovery_next_actions(
Some(operation_retry_action(operation_id))
}
RadrootsSdkRecoveryAction::EnableRequiredFeature => {
- Some("radroots health status get".to_owned())
+ Some("radroots health inspect".to_owned())
}
_ => None,
})
@@ -667,14 +681,14 @@ fn account_failure_output(
default_error: impl FnOnce() -> OperationAdapterError,
) -> OperationAdapterError {
match detail_json {
- Some(detail_json) => OperationAdapterError::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: code.to_owned(),
- class: "account".to_owned(),
+ Some(detail_json) => OperationAdapterError::detailed_failure(
+ operation_id.to_owned(),
+ code.to_owned(),
+ "account".to_owned(),
message,
exit_code,
- detail_json: detail_json.to_owned(),
- },
+ detail_json.to_owned(),
+ ),
None => default_error(),
}
}
@@ -698,18 +712,18 @@ fn auth_runtime_failure(
"status 403",
],
);
- OperationAdapterError::DetailedFailure {
- operation_id: operation_id.to_owned(),
- code: if unauthorized {
+ OperationAdapterError::detailed_failure(
+ operation_id.to_owned(),
+ if unauthorized {
"auth_unauthorized".to_owned()
} else {
"auth_unavailable".to_owned()
},
- class: "auth".to_owned(),
+ "auth".to_owned(),
message,
- exit_code: CliExitCode::AuthorizationFailed,
- detail_json: Value::Null.to_string(),
- }
+ CliExitCode::AuthorizationFailed,
+ Value::Null.to_string(),
+ )
}
fn classify_runtime_failure(
@@ -926,7 +940,7 @@ mod tests {
#[test]
fn sdk_storage_error_maps_to_typed_output_without_string_classification() {
let error = OperationAdapterError::sdk_failure(
- "store.status.get",
+ "store.inspect",
RadrootsSdkError::EventStore {
message: "database is locked".to_owned(),
},
@@ -937,11 +951,11 @@ mod tests {
assert_eq!(output.code, "event_store");
assert_eq!(output.exit_code, CliExitCode::RuntimeUnavailable.code());
let detail = output.detail.expect("detail");
- assert_eq!(detail["operation_id"], "store.status.get");
+ assert_eq!(detail["operation_id"], "store.inspect");
assert_eq!(detail["class"], "storage");
assert_eq!(detail["retryable"], true);
assert_eq!(detail["detail"]["message"], "database is locked");
- assert_eq!(detail["actions"], json!(["radroots store status get"]));
+ assert_eq!(detail["actions"], json!(["radroots store inspect"]));
}
#[test]
diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs
@@ -12,13 +12,12 @@ use serde_json::{Value, json};
use crate::cli::global::{OrderDraftAdjustmentArgs, OrderDraftCreateArgs};
use crate::ops::{
- BasketAdjustmentAddRequest, BasketAdjustmentAddResult, BasketAdjustmentRemoveRequest,
- BasketAdjustmentRemoveResult, BasketCreateRequest, BasketCreateResult, BasketGetRequest,
- BasketGetResult, BasketItemAddRequest, BasketItemAddResult, BasketItemRemoveRequest,
- BasketItemRemoveResult, BasketItemUpdateRequest, BasketItemUpdateResult, BasketListRequest,
- BasketListResult, BasketQuoteCreateRequest, BasketQuoteCreateResult, BasketValidateRequest,
- BasketValidateResult, OperationAdapterError, OperationRequest, OperationRequestData,
- OperationRequestPayload, OperationResult, OperationResultData, OperationService,
+ BasketCreateRequest, BasketCreateResult, BasketGetRequest, BasketGetResult,
+ BasketItemAddRequest, BasketItemAddResult, BasketItemRemoveRequest, BasketItemRemoveResult,
+ BasketItemUpdateRequest, BasketItemUpdateResult, BasketListRequest, BasketListResult,
+ BasketQuoteRequest, BasketQuoteResult, OperationAdapterError, OperationRequest,
+ OperationRequestData, OperationRequestPayload, OperationResult, OperationResultData,
+ OperationService,
};
use crate::runtime::config::RuntimeConfig;
use crate::view::runtime::OrderNewView;
@@ -351,126 +350,12 @@ impl OperationService<BasketItemRemoveRequest> for BasketOperationService<'_> {
}
}
-impl OperationService<BasketAdjustmentAddRequest> for BasketOperationService<'_> {
- type Result = BasketAdjustmentAddResult;
+impl OperationService<BasketQuoteRequest> for BasketOperationService<'_> {
+ type Result = BasketQuoteResult;
fn execute(
&self,
- request: OperationRequest<BasketAdjustmentAddRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let mut loaded =
- load_required_basket(self.config, basket_id.as_str(), request.operation_id())?;
- let adjustment = required_adjustment_from_request(&request)?;
- if loaded
- .document
- .basket
- .adjustments
- .iter()
- .any(|existing| existing.id == adjustment.id)
- {
- return Err(invalid_input(
- request.operation_id(),
- format!("basket adjustment `{}` already exists", adjustment.id),
- ));
- }
- if request.context.dry_run {
- return json_operation_result::<BasketAdjustmentAddResult>(json!({
- "state": "dry_run",
- "source": BASKET_SOURCE,
- "basket_id": basket_id,
- "adjustment": adjustment,
- "actions": ["radroots basket adjustment add"],
- }));
- }
-
- loaded.document.basket.adjustments.push(adjustment);
- touch_basket(&mut loaded.document);
- loaded.document.quote = None;
- save_basket(loaded.file.as_path(), &loaded.document)?;
- json_operation_result::<BasketAdjustmentAddResult>(basket_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- Some("updated"),
- )?)
- }
-}
-
-impl OperationService<BasketAdjustmentRemoveRequest> for BasketOperationService<'_> {
- type Result = BasketAdjustmentRemoveResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketAdjustmentRemoveRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let adjustment_id = required_string(&request, "id")?;
- let mut loaded =
- load_required_basket(self.config, basket_id.as_str(), request.operation_id())?;
- let Some(index) = loaded
- .document
- .basket
- .adjustments
- .iter()
- .position(|adjustment| adjustment.id == adjustment_id)
- else {
- return Err(invalid_input(
- request.operation_id(),
- format!("basket adjustment `{adjustment_id}` was not found"),
- ));
- };
- if request.context.dry_run {
- return json_operation_result::<BasketAdjustmentRemoveResult>(json!({
- "state": "dry_run",
- "source": BASKET_SOURCE,
- "basket_id": basket_id,
- "adjustment_id": adjustment_id,
- "actions": ["radroots basket adjustment remove"],
- }));
- }
-
- loaded.document.basket.adjustments.remove(index);
- touch_basket(&mut loaded.document);
- loaded.document.quote = None;
- save_basket(loaded.file.as_path(), &loaded.document)?;
- json_operation_result::<BasketAdjustmentRemoveResult>(basket_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- Some("updated"),
- )?)
- }
-}
-
-impl OperationService<BasketValidateRequest> for BasketOperationService<'_> {
- type Result = BasketValidateResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketValidateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let Some(loaded) = load_basket_optional(self.config, basket_id.as_str())? else {
- return json_operation_result::<BasketValidateResult>(missing_basket_view(
- self.config,
- basket_id.as_str(),
- ));
- };
- json_operation_result::<BasketValidateResult>(basket_validation_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- )?)
- }
-}
-
-impl OperationService<BasketQuoteCreateRequest> for BasketOperationService<'_> {
- type Result = BasketQuoteCreateResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketQuoteCreateRequest>,
+ request: OperationRequest<BasketQuoteRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let basket_id = required_basket_id(&request)?;
let mut loaded =
@@ -478,7 +363,7 @@ impl OperationService<BasketQuoteCreateRequest> for BasketOperationService<'_> {
let issues = basket_issues(self.config, &loaded.document)?;
if !issues.is_empty() {
let actions = basket_actions(&loaded.document, issues.as_slice());
- return json_operation_result::<BasketQuoteCreateResult>(json!({
+ return json_operation_result::<BasketQuoteResult>(json!({
"state": "unconfigured",
"source": BASKET_QUOTE_SOURCE,
"basket_id": basket_id,
@@ -510,7 +395,7 @@ impl OperationService<BasketQuoteCreateRequest> for BasketOperationService<'_> {
.map_err(|error| {
OperationAdapterError::runtime_failure(request.operation_id(), error)
})?;
- return json_operation_result::<BasketQuoteCreateResult>(json!({
+ return json_operation_result::<BasketQuoteResult>(json!({
"state": "dry_run",
"source": BASKET_QUOTE_SOURCE,
"basket_id": basket_id,
@@ -553,7 +438,7 @@ impl OperationService<BasketQuoteCreateRequest> for BasketOperationService<'_> {
touch_basket(&mut loaded.document);
save_basket(loaded.file.as_path(), &loaded.document)?;
- json_operation_result::<BasketQuoteCreateResult>(json!({
+ json_operation_result::<BasketQuoteResult>(json!({
"state": "quoted",
"source": BASKET_QUOTE_SOURCE,
"basket_id": loaded.document.basket.basket_id,
@@ -655,66 +540,6 @@ where
Ok(item)
}
-fn required_adjustment_from_request<P>(
- request: &OperationRequest<P>,
-) -> Result<BasketAdjustment, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- let id = required_string(request, "id")?.trim().to_owned();
- if id.is_empty() {
- return Err(invalid_input(
- request.operation_id(),
- "`id` must not be empty".to_owned(),
- ));
- }
- let effect = required_string(request, "effect")?.trim().to_owned();
- if effect != "increase" && effect != "decrease" {
- return Err(invalid_input(
- request.operation_id(),
- "`effect` must be increase or decrease".to_owned(),
- ));
- }
- let amount = required_string(request, "amount")?.trim().to_owned();
- let parsed_amount = amount
- .parse::<radroots_core::RadrootsCoreDecimal>()
- .map_err(|_| {
- invalid_input(
- request.operation_id(),
- "`amount` must be a valid decimal value".to_owned(),
- )
- })?;
- if parsed_amount.is_sign_negative() || parsed_amount.is_zero() {
- return Err(invalid_input(
- request.operation_id(),
- "`amount` must be greater than zero".to_owned(),
- ));
- }
- let currency = required_string(request, "currency")?
- .trim()
- .to_ascii_uppercase();
- if radroots_core::RadrootsCoreCurrency::from_str_upper(currency.as_str()).is_err() {
- return Err(invalid_input(
- request.operation_id(),
- "`currency` must be a valid ISO currency code".to_owned(),
- ));
- }
- let reason = required_string(request, "reason")?.trim().to_owned();
- if reason.is_empty() {
- return Err(invalid_input(
- request.operation_id(),
- "`reason` must not be empty".to_owned(),
- ));
- }
- Ok(BasketAdjustment {
- id,
- effect,
- amount,
- currency,
- reason,
- })
-}
-
fn basket_view(
config: &RuntimeConfig,
document: &BasketDocument,
@@ -740,27 +565,6 @@ fn basket_view(
}))
}
-fn basket_validation_view(
- config: &RuntimeConfig,
- document: &BasketDocument,
- file: &Path,
-) -> Result<Value, OperationAdapterError> {
- let issues = basket_issues(config, document)?;
- let ready_for_quote = issues.is_empty();
- let actions = basket_actions(document, issues.as_slice());
- Ok(json!({
- "state": if ready_for_quote { "ready" } else { "unconfigured" },
- "source": BASKET_SOURCE,
- "basket_id": document.basket.basket_id,
- "file": file.display().to_string(),
- "ready_for_quote": ready_for_quote,
- "item_count": document.basket.items.len(),
- "adjustment_count": document.basket.adjustments.len(),
- "issues": issues,
- "actions": actions,
- }))
-}
-
fn missing_basket_view(config: &RuntimeConfig, lookup: &str) -> Value {
json!({
"state": "missing",
@@ -877,7 +681,7 @@ fn basket_market_issues(
return Ok(vec![basket_issue(
"basket_market_replica_missing",
"local.replica_store",
- "current local replica data is required before quote creation; run `radroots store init` and `radroots market refresh`",
+ "current local replica data is required before quote creation; run `radroots store inspect` and `radroots market pull`",
)]);
}
let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path).map_err(|error| {
@@ -894,7 +698,7 @@ fn basket_market_issues(
issues.push(basket_issue(
"basket_item_listing_unresolved",
basket_item_listing_field(item),
- "basket item listing is not active in the current local replica; run `radroots market refresh` before quote creation",
+ "basket item listing is not active in the current local replica; run `radroots market pull` before quote creation",
));
continue;
}
@@ -1084,7 +888,7 @@ fn basket_actions(document: &BasketDocument, issues: &[BasketIssue]) -> Vec<Stri
fn quote_actions(order: &OrderNewView) -> Vec<String> {
if order.ready_for_submit {
- vec![format!("radroots trade submit {}", order.order_id)]
+ vec![format!("radroots trade request {}", order.order_id)]
} else {
let mut actions = vec![format!("radroots trade get {}", order.order_id)];
actions.extend(order.actions.iter().cloned());
@@ -1310,636 +1114,3 @@ fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
message,
}
}
-
-#[cfg(test)]
-mod tests {
- use std::path::{Path, PathBuf};
-
- use radroots_event::ids::RadrootsListingAddress;
- use radroots_event::kinds::{KIND_FARM, KIND_LISTING};
- use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts};
- use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
- use radroots_secret_vault::RadrootsSecretBackend;
- use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor};
- use serde_json::{Map, Value, json};
- use tempfile::tempdir;
-
- use super::BasketOperationService;
- use crate::ops::{
- BasketAdjustmentAddRequest, BasketAdjustmentRemoveRequest, BasketCreateRequest,
- BasketGetRequest, BasketItemAddRequest, BasketItemRemoveRequest, BasketItemUpdateRequest,
- BasketListRequest, BasketQuoteCreateRequest, BasketValidateRequest, OperationAdapter,
- OperationContext, OperationData, OperationRequest,
- };
- use crate::runtime::account;
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
-
- const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
- #[test]
- fn basket_service_creates_gets_and_lists_local_baskets() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- let create = OperationRequest::new(
- OperationContext::default(),
- BasketCreateRequest::from_data(data(&[("basket_id", "basket_test")])),
- )
- .expect("basket create request");
- let create_envelope = service
- .execute(create)
- .expect("basket create result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_create"))
- .expect("basket create envelope");
- assert_eq!(create_envelope.operation_id, "basket.create");
- assert_eq!(create_envelope.result["basket_id"], "basket_test");
- assert_eq!(create_envelope.result["item_count"], 0);
-
- let get = OperationRequest::new(
- OperationContext::default(),
- BasketGetRequest::from_data(data(&[("basket_id", "basket_test")])),
- )
- .expect("basket get request");
- let get_envelope = service
- .execute(get)
- .expect("basket get result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_get"))
- .expect("basket get envelope");
- assert_eq!(get_envelope.operation_id, "basket.get");
- assert_eq!(get_envelope.result["state"], "ready");
-
- let list = OperationRequest::new(OperationContext::default(), BasketListRequest::default())
- .expect("basket list request");
- let list_envelope = service
- .execute(list)
- .expect("basket list result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_list"))
- .expect("basket list envelope");
- assert_eq!(list_envelope.operation_id, "basket.list");
- assert_eq!(list_envelope.result["count"], 1);
- }
-
- #[test]
- fn basket_service_mutates_items_and_validates_readiness() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_items");
-
- let add = OperationRequest::new(
- OperationContext::default(),
- BasketItemAddRequest::from_data(data(&[
- ("basket_id", "basket_items"),
- ("listing_addr", LISTING_ADDR),
- ("bin_id", "bin-1"),
- ("quantity", "2"),
- ])),
- )
- .expect("basket item add request");
- let add_envelope = service
- .execute(add)
- .expect("basket item add result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_add"))
- .expect("basket item add envelope");
- assert_eq!(add_envelope.operation_id, "basket.item.add");
- assert_eq!(add_envelope.result["item_count"], 1);
-
- let update = OperationRequest::new(
- OperationContext::default(),
- BasketItemUpdateRequest::from_data(data(&[
- ("basket_id", "basket_items"),
- ("item_id", "item_1"),
- ("quantity", "3"),
- ])),
- )
- .expect("basket item update request");
- let update_envelope = service
- .execute(update)
- .expect("basket item update result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_update"))
- .expect("basket item update envelope");
- assert_eq!(update_envelope.operation_id, "basket.item.update");
- assert_eq!(update_envelope.result["items"][0]["quantity"], 3);
-
- let validate = OperationRequest::new(
- OperationContext::default(),
- BasketValidateRequest::from_data(data(&[("basket_id", "basket_items")])),
- )
- .expect("basket validate request");
- let validate_envelope = service
- .execute(validate)
- .expect("basket validate result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_validate"))
- .expect("basket validate envelope");
- assert_eq!(validate_envelope.operation_id, "basket.validate");
- assert_eq!(validate_envelope.result["ready_for_quote"], false);
- assert_eq!(
- validate_envelope.result["issues"][0]["code"],
- "basket_market_replica_missing"
- );
-
- let adjustment_add = OperationRequest::new(
- OperationContext::default(),
- BasketAdjustmentAddRequest::from_data(data(&[
- ("basket_id", "basket_items"),
- ("id", "adj_pickup"),
- ("effect", "decrease"),
- ("amount", "1.00"),
- ("currency", "USD"),
- ("reason", "pickup"),
- ])),
- )
- .expect("basket adjustment add request");
- let adjustment_add_envelope = service
- .execute(adjustment_add)
- .expect("basket adjustment add result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_adjust_add"))
- .expect("basket adjustment add envelope");
- assert_eq!(
- adjustment_add_envelope.operation_id,
- "basket.adjustment.add"
- );
- assert_eq!(adjustment_add_envelope.result["adjustment_count"], 1);
-
- let adjustment_remove = OperationRequest::new(
- OperationContext::default(),
- BasketAdjustmentRemoveRequest::from_data(data(&[
- ("basket_id", "basket_items"),
- ("id", "adj_pickup"),
- ])),
- )
- .expect("basket adjustment remove request");
- let adjustment_remove_envelope = service
- .execute(adjustment_remove)
- .expect("basket adjustment remove result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_adjust_remove"))
- .expect("basket adjustment remove envelope");
- assert_eq!(
- adjustment_remove_envelope.operation_id,
- "basket.adjustment.remove"
- );
- assert_eq!(adjustment_remove_envelope.result["adjustment_count"], 0);
-
- let remove = OperationRequest::new(
- OperationContext::default(),
- BasketItemRemoveRequest::from_data(data(&[
- ("basket_id", "basket_items"),
- ("item_id", "item_1"),
- ])),
- )
- .expect("basket item remove request");
- let remove_envelope = service
- .execute(remove)
- .expect("basket item remove result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_remove"))
- .expect("basket item remove envelope");
- assert_eq!(remove_envelope.operation_id, "basket.item.remove");
- assert_eq!(remove_envelope.result["item_count"], 0);
- assert_eq!(remove_envelope.result["ready_for_quote"], false);
- }
-
- #[test]
- fn basket_quote_create_materializes_order_draft() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- seed_current_listing(&config);
- account::create_default_account(&config).expect("create buyer account");
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_quote");
- add_listing_item(&service, "basket_quote");
-
- let quote = OperationRequest::new(
- OperationContext::default(),
- BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_quote")])),
- )
- .expect("basket quote request");
- let envelope = service
- .execute(quote)
- .expect("basket quote result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
- .expect("basket quote envelope");
-
- assert_eq!(envelope.operation_id, "basket.quote.create");
- assert_eq!(envelope.result["state"], "quoted");
- assert!(
- envelope.result["quote"]["trade_id"]
- .as_str()
- .unwrap()
- .starts_with("ord_")
- );
- assert!(
- envelope.result["trade"]["buyer_account_id"]
- .as_str()
- .expect("buyer account id")
- .len()
- > 8
- );
- assert!(
- envelope.result["trade"]["buyer_pubkey"]
- .as_str()
- .expect("buyer pubkey")
- .len()
- == 64
- );
- assert_eq!(
- envelope.result["trade"]["buyer_actor_source"],
- "resolved_account"
- );
- let order_file = PathBuf::from(envelope.result["quote"]["trade_file"].as_str().unwrap());
- assert!(order_file.exists());
- let draft = std::fs::read_to_string(order_file).expect("read order draft");
- assert!(draft.contains("[buyer_actor]"));
- assert!(draft.contains("source = \"resolved_account\""));
- }
-
- #[test]
- fn basket_quote_create_dry_run_skips_order_draft() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- seed_current_listing(&config);
- account::create_default_account(&config).expect("create buyer account");
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_dry_run");
- add_listing_item(&service, "basket_dry_run");
-
- let mut context = OperationContext::default();
- context.dry_run = true;
- let quote = OperationRequest::new(
- context.clone(),
- BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_dry_run")])),
- )
- .expect("basket quote request");
- let envelope = service
- .execute(quote)
- .expect("basket quote dry run")
- .to_envelope(context.envelope_context("req_basket_quote"))
- .expect("basket quote envelope");
-
- assert_eq!(envelope.operation_id, "basket.quote.create");
- assert_eq!(envelope.dry_run, true);
- assert_eq!(envelope.result["state"], "dry_run");
- assert_eq!(envelope.result["trade"]["state"], "dry_run");
- assert_eq!(
- envelope.result["trade"]["buyer_actor_source"],
- "resolved_account"
- );
- assert!(!PathBuf::from(envelope.result["trade"]["file"].as_str().unwrap()).exists());
- }
-
- #[test]
- fn basket_quote_create_requires_resolved_buyer_account() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- seed_current_listing(&config);
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_no_buyer");
- add_listing_item(&service, "basket_no_buyer");
-
- let quote = OperationRequest::new(
- OperationContext::default(),
- BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_no_buyer")])),
- )
- .expect("basket quote request");
- let error = service.execute(quote).expect_err("missing buyer account");
-
- let output_error = error.to_output_error();
- assert_eq!(output_error.code, "account_unresolved");
- let detail = output_error.detail.expect("account detail");
- assert_eq!(detail["buyer_actor_source"], "resolved_account");
- assert_eq!(detail["actions"][0], "radroots account create");
- }
-
- #[test]
- fn basket_readiness_fails_closed_without_replica_data() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_missing_replica");
- let add = add_listing_item(&service, "basket_missing_replica");
- assert_eq!(add.result["ready_for_quote"], false);
- assert_eq!(
- add.result["issues"][0]["code"],
- "basket_market_replica_missing"
- );
-
- let list = OperationRequest::new(OperationContext::default(), BasketListRequest::default())
- .expect("basket list request");
- let list_envelope = service
- .execute(list)
- .expect("basket list result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_list"))
- .expect("basket list envelope");
- assert_eq!(
- list_envelope.result["baskets"][0]["issues"][0]["code"],
- "basket_market_replica_missing"
- );
-
- let validate = OperationRequest::new(
- OperationContext::default(),
- BasketValidateRequest::from_data(data(&[("basket_id", "basket_missing_replica")])),
- )
- .expect("basket validate request");
- let validate_envelope = service
- .execute(validate)
- .expect("basket validate result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_validate"))
- .expect("basket validate envelope");
- assert_eq!(validate_envelope.result["state"], "unconfigured");
- assert_eq!(
- validate_envelope.result["issues"][0]["code"],
- "basket_market_replica_missing"
- );
-
- let quote = OperationRequest::new(
- OperationContext::default(),
- BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_missing_replica")])),
- )
- .expect("basket quote request");
- let quote_envelope = service
- .execute(quote)
- .expect("basket quote result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
- .expect("basket quote envelope");
- assert_eq!(quote_envelope.result["state"], "unconfigured");
- assert_eq!(
- quote_envelope.result["issues"][0]["code"],
- "basket_market_replica_missing"
- );
- assert!(!config.paths.app_data_root.join("orders/drafts").exists());
- }
-
- #[test]
- fn basket_readiness_fails_closed_for_unresolved_listing() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- crate::runtime::store::init(&config).expect("store init");
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_unresolved");
- let add = add_listing_item(&service, "basket_unresolved");
- assert_eq!(add.result["ready_for_quote"], false);
- assert_eq!(
- add.result["issues"][0]["code"],
- "basket_item_listing_unresolved"
- );
-
- let quote = OperationRequest::new(
- OperationContext::default(),
- BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_unresolved")])),
- )
- .expect("basket quote request");
- let quote_envelope = service
- .execute(quote)
- .expect("basket quote result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
- .expect("basket quote envelope");
- assert_eq!(quote_envelope.result["state"], "unconfigured");
- assert_eq!(
- quote_envelope.result["issues"][0]["code"],
- "basket_item_listing_unresolved"
- );
- assert!(!config.paths.app_data_root.join("orders/drafts").exists());
- }
-
- #[test]
- fn basket_readiness_fails_closed_for_ambiguous_listing() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- seed_current_listing(&config);
- duplicate_current_listing_row(&config);
- let service = OperationAdapter::new(BasketOperationService::new(&config));
- create_basket(&service, "basket_ambiguous");
- let add = add_listing_item(&service, "basket_ambiguous");
- assert_eq!(add.result["ready_for_quote"], false);
- assert_eq!(
- add.result["issues"][0]["code"],
- "basket_item_listing_ambiguous"
- );
-
- let quote = OperationRequest::new(
- OperationContext::default(),
- BasketQuoteCreateRequest::from_data(data(&[("basket_id", "basket_ambiguous")])),
- )
- .expect("basket quote request");
- let quote_envelope = service
- .execute(quote)
- .expect("basket quote result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_quote"))
- .expect("basket quote envelope");
- assert_eq!(quote_envelope.result["state"], "unconfigured");
- assert_eq!(
- quote_envelope.result["issues"][0]["code"],
- "basket_item_listing_ambiguous"
- );
- assert!(!config.paths.app_data_root.join("orders/drafts").exists());
- }
-
- fn create_basket(service: &OperationAdapter<BasketOperationService<'_>>, basket_id: &str) {
- let request = OperationRequest::new(
- OperationContext::default(),
- BasketCreateRequest::from_data(data(&[("basket_id", basket_id)])),
- )
- .expect("basket create request");
- service.execute(request).expect("basket create result");
- }
-
- fn add_listing_item(
- service: &OperationAdapter<BasketOperationService<'_>>,
- basket_id: &str,
- ) -> crate::out::envelope::OutputEnvelope {
- let request = OperationRequest::new(
- OperationContext::default(),
- BasketItemAddRequest::from_data(data(&[
- ("basket_id", basket_id),
- ("listing_addr", LISTING_ADDR),
- ("bin_id", "bin-1"),
- ("quantity", "1"),
- ])),
- )
- .expect("basket item add request");
- service
- .execute(request)
- .expect("basket item add result")
- .to_envelope(OperationContext::default().envelope_context("req_basket_add"))
- .expect("basket item add envelope")
- }
-
- fn seed_current_listing(config: &RuntimeConfig) {
- crate::runtime::store::init(config).expect("store init");
- let (seller_pubkey, listing_id) = listing_addr_parts(LISTING_ADDR);
- let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: "2".repeat(64),
- author: seller_pubkey.clone(),
- created_at: 1,
- kind: KIND_LISTING,
- tags: vec![
- vec!["d".to_owned(), listing_id],
- vec![
- "a".to_owned(),
- format!(
- "{}:{}:{}",
- KIND_FARM, seller_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"
- ),
- ],
- vec!["p".to_owned(), seller_pubkey],
- vec!["key".to_owned(), "pasture-eggs".to_owned()],
- vec!["title".to_owned(), "Market Eggs".to_owned()],
- vec!["category".to_owned(), "eggs".to_owned()],
- vec!["summary".to_owned(), "Pasture-raised eggs".to_owned()],
- vec!["process".to_owned(), "washed".to_owned()],
- vec!["lot".to_owned(), "lot-a".to_owned()],
- vec!["profile".to_owned(), "dozen".to_owned()],
- vec!["year".to_owned(), "2026".to_owned()],
- vec!["radroots:primary_bin".to_owned(), "bin-1".to_owned()],
- vec![
- "radroots:bin".to_owned(),
- "bin-1".to_owned(),
- "12".to_owned(),
- "each".to_owned(),
- "12".to_owned(),
- "each".to_owned(),
- "dozen".to_owned(),
- ],
- vec![
- "radroots:price".to_owned(),
- "bin-1".to_owned(),
- "6".to_owned(),
- "USD".to_owned(),
- "1".to_owned(),
- "each".to_owned(),
- "6".to_owned(),
- "each".to_owned(),
- ],
- vec!["inventory".to_owned(), "5".to_owned()],
- vec!["status".to_owned(), "active".to_owned()],
- ],
- content: "# Market Eggs".to_owned(),
- sig: "f".repeat(128),
- })
- .expect("seed listing event");
- let executor =
- SqlxSqliteExecutor::open(&config.local.replica_store_path).expect("open replica");
- assert_eq!(
- radroots_replica_ingest_event(&executor, &event).expect("ingest listing"),
- RadrootsReplicaIngestOutcome::Applied
- );
- }
-
- fn listing_addr_parts(listing_addr: &str) -> (String, String) {
- let parsed = RadrootsListingAddress::parse(listing_addr).expect("listing addr");
- let (_, rest) = parsed.as_str().split_once(':').expect("listing addr kind");
- let (seller_pubkey, listing_id) = rest.split_once(':').expect("listing addr parts");
- (seller_pubkey.to_owned(), listing_id.to_owned())
- }
-
- fn duplicate_current_listing_row(config: &RuntimeConfig) {
- let executor =
- SqlxSqliteExecutor::open(&config.local.replica_store_path).expect("open replica");
- let params = json!(["33333333-3333-3333-3333-333333333333", LISTING_ADDR]).to_string();
- executor
- .exec(
- "INSERT INTO trade_product (id, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id) SELECT ?, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id FROM trade_product WHERE listing_addr = ?;",
- params.as_str(),
- )
- .expect("duplicate listing row");
- }
-
- fn sample_config(root: &Path) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-
- fn data(entries: &[(&str, &str)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
- .collect::<Map<String, Value>>()
- }
-}
diff --git a/src/ops/exec/core.rs b/src/ops/exec/core.rs
@@ -4,29 +4,23 @@ use radroots_transport::RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE;
use serde::Serialize;
use serde_json::{Value, json};
-use crate::cli::global::LocalExportFormatArg;
use crate::ops::{
- AccountAttachSecretRequest, AccountAttachSecretResult, AccountCreateRequest,
- AccountCreateResult, AccountGetRequest, AccountGetResult, AccountImportRequest,
- AccountImportResult, AccountListRequest, AccountListResult, AccountRemoveRequest,
- AccountRemoveResult, AccountSelectionClearRequest, AccountSelectionClearResult,
- AccountSelectionGetRequest, AccountSelectionGetResult, AccountSelectionUpdateRequest,
- AccountSelectionUpdateResult, ConfigGetRequest, ConfigGetResult, HealthCheckRunRequest,
- HealthCheckRunResult, HealthStatusGetRequest, HealthStatusGetResult, OperationAdapterError,
- OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult,
- OperationResultData, OperationService, StoreBackupCreateRequest, StoreBackupCreateResult,
- StoreBackupRestoreRequest, StoreBackupRestoreResult, StoreExportRequest, StoreExportResult,
- StoreInitRequest, StoreInitResult, StoreStatusGetRequest, StoreStatusGetResult,
- WorkspaceGetRequest, WorkspaceGetResult, WorkspaceInitRequest, WorkspaceInitResult,
+ AccountCreateRequest, AccountCreateResult, AccountImportRequest, AccountImportResult,
+ AccountListRequest, AccountListResult, AccountRemoveRequest, AccountRemoveResult,
+ AccountSelectRequest, AccountSelectResult, HealthInspectRequest, HealthInspectResult,
+ OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
+ OperationResult, OperationResultData, OperationService, ProfileInspectRequest,
+ ProfileInspectResult, ProfileResetRequest, ProfileResetResult, StoreBackupRequest,
+ StoreBackupResult, StoreInspectRequest, StoreInspectResult, StoreRestoreRequest,
+ StoreRestoreResult,
};
use crate::out::envelope::OutputWarning;
use crate::runtime::RuntimeError;
use crate::runtime::account::{
AccountResolution, AccountRuntimeFailure, account_resolution_view, account_summary_view,
- attach_identity_secret, clear_default_account, create_default_account, import_public_identity,
- preview_account_removal, preview_identity_secret_attachment, preview_public_identity_import,
- remove_account, resolve_account_resolution, resolve_account_selector, secret_backend_status,
- select_account, snapshot, unresolved_account_reason,
+ create_default_account, import_public_identity, preview_account_removal,
+ preview_public_identity_import, remove_account, resolve_account_resolution,
+ resolve_account_selector, secret_backend_status, select_account, snapshot,
};
use crate::runtime::config::{RuntimeConfig, SignerBackend, TransportProfileKind};
use crate::runtime::logging::LoggingState;
@@ -48,16 +42,16 @@ impl<'a> CoreOperationService<'a> {
}
}
-impl OperationService<WorkspaceInitRequest> for CoreOperationService<'_> {
- type Result = WorkspaceInitResult;
+impl OperationService<ProfileResetRequest> for CoreOperationService<'_> {
+ type Result = ProfileResetResult;
fn execute(
&self,
- request: OperationRequest<WorkspaceInitRequest>,
+ request: OperationRequest<ProfileResetRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
if request.context.dry_run {
let local = map_runtime(crate::runtime::store::init_preflight(self.config))?;
- return json_operation_result::<WorkspaceInitResult>(json!({
+ return json_operation_result::<ProfileResetResult>(json!({
"state": local.state,
"profile": self.config.paths.profile,
"local": local,
@@ -65,7 +59,7 @@ impl OperationService<WorkspaceInitRequest> for CoreOperationService<'_> {
}
let local = map_runtime(crate::runtime::store::init(self.config))?;
- json_operation_result::<WorkspaceInitResult>(json!({
+ json_operation_result::<ProfileResetResult>(json!({
"state": local.state,
"profile": self.config.paths.profile,
"local": local,
@@ -73,14 +67,14 @@ impl OperationService<WorkspaceInitRequest> for CoreOperationService<'_> {
}
}
-impl OperationService<WorkspaceGetRequest> for CoreOperationService<'_> {
- type Result = WorkspaceGetResult;
+impl OperationService<ProfileInspectRequest> for CoreOperationService<'_> {
+ type Result = ProfileInspectResult;
fn execute(
&self,
- _request: OperationRequest<WorkspaceGetRequest>,
+ _request: OperationRequest<ProfileInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- json_operation_result::<WorkspaceGetResult>(json!({
+ json_operation_result::<ProfileInspectResult>(json!({
"profile": self.config.paths.profile,
"profile_source": self.config.paths.profile_source,
"root_source": self.config.paths.root_source,
@@ -96,12 +90,12 @@ impl OperationService<WorkspaceGetRequest> for CoreOperationService<'_> {
}
}
-impl OperationService<HealthStatusGetRequest> for CoreOperationService<'_> {
- type Result = HealthStatusGetResult;
+impl OperationService<HealthInspectRequest> for CoreOperationService<'_> {
+ type Result = HealthInspectResult;
fn execute(
&self,
- request: OperationRequest<HealthStatusGetRequest>,
+ request: OperationRequest<HealthInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let store = map_sdk_adapter(
request.operation_id(),
@@ -112,7 +106,7 @@ impl OperationService<HealthStatusGetRequest> for CoreOperationService<'_> {
let signer = signer_health_view(self.config, &account);
let state = health_status_state(&store.state, &publish);
let actions = health_actions(self.config, store.state.as_str(), &account, &publish);
- json_operation_result::<HealthStatusGetResult>(json!({
+ json_operation_result::<HealthInspectResult>(json!({
"state": state,
"store": store,
"account_resolution": account_resolution_view(&account),
@@ -127,138 +121,6 @@ impl OperationService<HealthStatusGetRequest> for CoreOperationService<'_> {
}
}
-impl OperationService<HealthCheckRunRequest> for CoreOperationService<'_> {
- type Result = HealthCheckRunResult;
-
- fn execute(
- &self,
- request: OperationRequest<HealthCheckRunRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let store = map_sdk_adapter(
- request.operation_id(),
- crate::runtime::store::status(self.config),
- )?;
- let account = map_runtime(resolve_account_resolution(self.config))?;
- let account_reason = if account.resolved_account.is_some() {
- None
- } else {
- Some(map_runtime(unresolved_account_reason(self.config))?)
- };
- let publish = publish_runtime_view(self.config, true, &account);
- let signer = signer_health_view(self.config, &account);
- let state = health_check_state(&store.state, account.resolved_account.is_some(), &publish);
- let actions = health_actions(self.config, store.state.as_str(), &account, &publish);
- json_operation_result::<HealthCheckRunResult>(json!({
- "state": state,
- "account_resolution": account_resolution_view(&account),
- "checks": {
- "workspace": {
- "state": "ready",
- "profile": self.config.paths.profile,
- },
- "store": {
- "state": store.state,
- "source": store.source,
- "canonical_store": store.canonical_store,
- "sdk_storage": store.sdk_storage,
- "sdk_root": store.sdk_root,
- "sdk_existed_before_open": store.sdk_existed_before_open,
- "event_store": store.event_store,
- "outbox": store.outbox,
- "integrity": store.integrity,
- "derived_projection": store.derived_projection,
- "reason": store.reason,
- },
- "account": {
- "state": if account.resolved_account.is_some() { "ready" } else { "unconfigured" },
- "reason": account_reason,
- },
- "signer": signer,
- "publish": {
- "state": publish.state,
- "transport": publish.transport,
- "executable": publish.executable,
- "reason": publish.reason,
- },
- },
- "actions": actions,
- }))
- }
-}
-
-impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
- type Result = ConfigGetResult;
-
- fn execute(
- &self,
- _request: OperationRequest<ConfigGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let account = map_runtime(resolve_account_resolution(self.config))?;
- let publish = publish_runtime_view(self.config, true, &account);
- let write_plane =
- crate::runtime::provider::resolve_write_plane_provider(self.config, &publish);
- let actions = config_actions(self.config, &account, &publish);
- let result = json!({
- "output": {
- "format": self.config.output.format.as_str(),
- "verbosity": self.config.output.verbosity.as_str(),
- "dry_run": self.config.output.dry_run,
- },
- "interaction": {
- "input_enabled": self.config.interaction.input_enabled,
- "prompts_allowed": self.config.interaction.prompts_allowed,
- "confirmations_allowed": self.config.interaction.confirmations_allowed,
- },
- "paths": {
- "profile": self.config.paths.profile,
- "app_config_path": self.config.paths.app_config_path.display().to_string(),
- "workspace_config_path": self.config.paths.workspace_config_path.as_ref().map(|path| path.display().to_string()),
- "app_data_root": self.config.paths.app_data_root.display().to_string(),
- "shared_cache_root": self.config.paths.shared_cache_root.display().to_string(),
- "app_logs_root": self.config.paths.app_logs_root.display().to_string(),
- },
- "account": {
- "selector": self.config.account.selector,
- "store_path": self.config.account.store_path.display().to_string(),
- "secrets_dir": self.config.account.secrets_dir.display().to_string(),
- },
- "account_resolution": account_resolution_view(&account),
- "signer": {
- "mode": self.config.signer.backend.as_str(),
- },
- "transport": crate::runtime::transport::profile(self.config),
- "mesh": crate::runtime::mesh::scope(self.config),
- "publish": publish,
- "myc": {
- "executable": self.config.myc.executable.display().to_string(),
- "status_timeout_ms": self.config.myc.status_timeout_ms,
- },
- "hyf": {
- "enabled": self.config.hyf.enabled,
- "executable": self.config.hyf.executable.display().to_string(),
- },
- "write_plane": {
- "provider_runtime_id": write_plane.provider_runtime_id,
- "binding_model": write_plane.binding_model,
- "state": write_plane.state,
- "provenance": write_plane.provenance,
- "source": write_plane.source,
- "target_kind": write_plane.target_kind,
- "target": write_plane.target,
- "detail": write_plane.detail,
- },
- "local": {
- "root": self.config.local.root.display().to_string(),
- "replica_store_path": self.config.local.replica_store_path.display().to_string(),
- "backups_dir": self.config.local.backups_dir.display().to_string(),
- "exports_dir": self.config.local.exports_dir.display().to_string(),
- },
- "actions": actions,
- });
- json_operation_result::<ConfigGetResult>(result)
- }
-}
-
impl OperationService<AccountCreateRequest> for CoreOperationService<'_> {
type Result = AccountCreateResult;
@@ -316,11 +178,6 @@ impl OperationService<AccountImportRequest> for CoreOperationService<'_> {
"account": account_summary_view(&account),
}));
}
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
let account = map_expected_runtime(
request.operation_id(),
@@ -333,91 +190,6 @@ impl OperationService<AccountImportRequest> for CoreOperationService<'_> {
}
}
-impl OperationService<AccountAttachSecretRequest> for CoreOperationService<'_> {
- type Result = AccountAttachSecretResult;
-
- fn execute(
- &self,
- request: OperationRequest<AccountAttachSecretRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let selector = required_string(&request, "selector")?;
- let path = required_path(&request, "path")?;
- let make_default = bool_input(&request, "default").unwrap_or(false);
- if request.context.dry_run {
- let secret_backend = account_secret_backend_ready(request.operation_id(), self.config)?;
- let account = map_expected_runtime(
- request.operation_id(),
- preview_identity_secret_attachment(
- self.config,
- selector.as_str(),
- path.as_path(),
- make_default,
- ),
- )?;
- return json_operation_result::<AccountAttachSecretResult>(json!({
- "state": "dry_run",
- "path": path.display().to_string(),
- "default": make_default,
- "secret_backend": {
- "state": secret_backend.state,
- "active_backend": secret_backend.active_backend,
- },
- "account": account_summary_view(&account),
- }));
- }
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
-
- let secret_backend = account_secret_backend_ready(request.operation_id(), self.config)?;
- let account = map_expected_runtime(
- request.operation_id(),
- attach_identity_secret(self.config, selector.as_str(), path.as_path(), make_default),
- )?;
- json_operation_result::<AccountAttachSecretResult>(json!({
- "state": "secret_attached",
- "default": make_default,
- "secret_backend": {
- "state": secret_backend.state,
- "active_backend": secret_backend.active_backend,
- },
- "account": account_summary_view(&account),
- }))
- }
-}
-
-impl OperationService<AccountGetRequest> for CoreOperationService<'_> {
- type Result = AccountGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<AccountGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let scoped;
- let config = if let Some(selector) = string_input(&request, "selector") {
- scoped = selected_config(self.config, selector);
- &scoped
- } else {
- self.config
- };
- let resolution = resolve_account_resolution(config).map_err(|error| {
- OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
- })?;
- let reason = if resolution.resolved_account.is_some() {
- None
- } else {
- Some(map_runtime(unresolved_account_reason(config))?)
- };
- json_operation_result::<AccountGetResult>(json!({
- "state": if resolution.resolved_account.is_some() { "ready" } else { "unconfigured" },
- "reason": reason,
- "account_resolution": account_resolution_view(&resolution),
- }))
- }
-}
-
impl OperationService<AccountListRequest> for CoreOperationService<'_> {
type Result = AccountListResult;
@@ -459,11 +231,6 @@ impl OperationService<AccountRemoveRequest> for CoreOperationService<'_> {
"remaining_account_count": preview.remaining_account_count,
}));
}
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
let resolved_farm_config =
map_runtime(crate::runtime::farm_config::load(self.config, None))?;
@@ -491,26 +258,12 @@ impl OperationService<AccountRemoveRequest> for CoreOperationService<'_> {
}
}
-impl OperationService<AccountSelectionGetRequest> for CoreOperationService<'_> {
- type Result = AccountSelectionGetResult;
-
- fn execute(
- &self,
- _request: OperationRequest<AccountSelectionGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let resolution = map_runtime(resolve_account_resolution(self.config))?;
- json_operation_result::<AccountSelectionGetResult>(json!({
- "account_resolution": account_resolution_view(&resolution),
- }))
- }
-}
-
-impl OperationService<AccountSelectionUpdateRequest> for CoreOperationService<'_> {
- type Result = AccountSelectionUpdateResult;
+impl OperationService<AccountSelectRequest> for CoreOperationService<'_> {
+ type Result = AccountSelectResult;
fn execute(
&self,
- request: OperationRequest<AccountSelectionUpdateRequest>,
+ request: OperationRequest<AccountSelectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let selector = required_string(&request, "selector")?;
if request.context.dry_run {
@@ -518,7 +271,7 @@ impl OperationService<AccountSelectionUpdateRequest> for CoreOperationService<'_
resolve_account_selector(self.config, selector.as_str()).map_err(|error| {
OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
})?;
- return json_operation_result::<AccountSelectionUpdateResult>(json!({
+ return json_operation_result::<AccountSelectResult>(json!({
"state": "dry_run",
"account": account_summary_view(&account),
}));
@@ -527,112 +280,34 @@ impl OperationService<AccountSelectionUpdateRequest> for CoreOperationService<'_
let account = select_account(self.config, selector.as_str()).map_err(|error| {
OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
})?;
- json_operation_result::<AccountSelectionUpdateResult>(json!({
+ json_operation_result::<AccountSelectResult>(json!({
"state": "default",
"account": account_summary_view(&account),
}))
}
}
-impl OperationService<AccountSelectionClearRequest> for CoreOperationService<'_> {
- type Result = AccountSelectionClearResult;
+impl OperationService<StoreInspectRequest> for CoreOperationService<'_> {
+ type Result = StoreInspectResult;
fn execute(
&self,
- request: OperationRequest<AccountSelectionClearRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.dry_run {
- let resolution = map_runtime(resolve_account_resolution(self.config))?;
- let account_snapshot = map_runtime(snapshot(self.config))?;
- return json_operation_result::<AccountSelectionClearResult>(json!({
- "state": "dry_run",
- "cleared_account": resolution.default_account.as_ref().map(account_summary_view),
- "remaining_account_count": account_snapshot.accounts.len(),
- }));
- }
-
- let result = map_runtime(clear_default_account(self.config))?;
- json_operation_result::<AccountSelectionClearResult>(json!({
- "state": if result.cleared_account.is_some() { "cleared" } else { "already_clear" },
- "cleared_account": result.cleared_account.as_ref().map(account_summary_view),
- "remaining_account_count": result.remaining_account_count,
- }))
- }
-}
-
-impl OperationService<StoreInitRequest> for CoreOperationService<'_> {
- type Result = StoreInitResult;
-
- fn execute(
- &self,
- request: OperationRequest<StoreInitRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.dry_run {
- let view = map_runtime(crate::runtime::store::init_preflight(self.config))?;
- return serialized_operation_result::<StoreInitResult, _>(&view);
- }
-
- let view = map_runtime(crate::runtime::store::init(self.config))?;
- serialized_operation_result::<StoreInitResult, _>(&view)
- }
-}
-
-impl OperationService<StoreStatusGetRequest> for CoreOperationService<'_> {
- type Result = StoreStatusGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<StoreStatusGetRequest>,
+ request: OperationRequest<StoreInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let view = map_sdk_adapter(
request.operation_id(),
crate::runtime::store::status(self.config),
)?;
- serialized_operation_result::<StoreStatusGetResult, _>(&view)
- }
-}
-
-impl OperationService<StoreExportRequest> for CoreOperationService<'_> {
- type Result = StoreExportResult;
-
- fn execute(
- &self,
- request: OperationRequest<StoreExportRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let output = optional_path(&request, "output")
- .unwrap_or_else(|| self.config.local.exports_dir.join("store-export.json"));
- let format = match string_input(&request, "format").as_deref() {
- Some("ndjson") => LocalExportFormatArg::Ndjson,
- Some("json") | None => LocalExportFormatArg::Json,
- Some(other) => {
- return Err(invalid_input(
- request.operation_id(),
- format!("format must be `json` or `ndjson`, got `{other}`"),
- ));
- }
- };
- if request.context.dry_run {
- return Err(invalid_input(
- request.operation_id(),
- "`radroots store export` does not support --dry-run".to_owned(),
- ));
- }
-
- let view = map_runtime(crate::runtime::store::export(
- self.config,
- format,
- output.as_path(),
- ))?;
- serialized_operation_result::<StoreExportResult, _>(&view)
+ serialized_operation_result::<StoreInspectResult, _>(&view)
}
}
-impl OperationService<StoreBackupCreateRequest> for CoreOperationService<'_> {
- type Result = StoreBackupCreateResult;
+impl OperationService<StoreBackupRequest> for CoreOperationService<'_> {
+ type Result = StoreBackupResult;
fn execute(
&self,
- request: OperationRequest<StoreBackupCreateRequest>,
+ request: OperationRequest<StoreBackupRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let output = optional_path(&request, "output")
.unwrap_or_else(|| self.config.local.backups_dir.join("sdk-store-backup"));
@@ -652,21 +327,16 @@ impl OperationService<StoreBackupCreateRequest> for CoreOperationService<'_> {
}
}
-impl OperationService<StoreBackupRestoreRequest> for CoreOperationService<'_> {
- type Result = StoreBackupRestoreResult;
+impl OperationService<StoreRestoreRequest> for CoreOperationService<'_> {
+ type Result = StoreRestoreResult;
fn execute(
&self,
- request: OperationRequest<StoreBackupRestoreRequest>,
+ request: OperationRequest<StoreRestoreRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let source = required_path(&request, "source")?;
let destination = optional_path(&request, "destination");
let overwrite = bool_input(&request, "overwrite").unwrap_or(false);
- if overwrite && request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
let view = map_sdk_adapter(
request.operation_id(),
@@ -708,23 +378,6 @@ fn map_sdk_adapter<T>(
result.map_err(|error| OperationAdapterError::sdk_adapter_failure(operation_id, error))
}
-fn account_secret_backend_ready(
- operation_id: &str,
- config: &RuntimeConfig,
-) -> Result<crate::runtime::account::AccountSecretBackendStatus, OperationAdapterError> {
- let secret_backend = secret_backend_status(config);
- if secret_backend.state == "ready" {
- return Ok(secret_backend);
- }
-
- Err(OperationAdapterError::OperationUnavailable {
- operation_id: operation_id.to_owned(),
- message: secret_backend
- .reason
- .unwrap_or_else(|| "account secret backend is not available".to_owned()),
- })
-}
-
fn map_expected_runtime<T>(
operation_id: &str,
result: Result<T, RuntimeError>,
@@ -735,11 +388,9 @@ fn map_expected_runtime<T>(
fn local_backup_result(
operation_id: &str,
view: &LocalBackupView,
-) -> Result<OperationResult<StoreBackupCreateResult>, OperationAdapterError> {
+) -> Result<OperationResult<StoreBackupResult>, OperationAdapterError> {
match view.disposition() {
- CommandDisposition::Success => {
- serialized_operation_result::<StoreBackupCreateResult, _>(view)
- }
+ CommandDisposition::Success => serialized_operation_result::<StoreBackupResult, _>(view),
disposition => Err(OperationAdapterError::from_command_disposition(
operation_id,
disposition,
@@ -759,11 +410,9 @@ fn local_backup_result(
fn local_restore_result(
operation_id: &str,
view: &LocalRestoreView,
-) -> Result<OperationResult<StoreBackupRestoreResult>, OperationAdapterError> {
+) -> Result<OperationResult<StoreRestoreResult>, OperationAdapterError> {
match view.disposition() {
- CommandDisposition::Success => {
- serialized_operation_result::<StoreBackupRestoreResult, _>(view)
- }
+ CommandDisposition::Success => serialized_operation_result::<StoreRestoreResult, _>(view),
disposition => Err(OperationAdapterError::from_command_disposition(
operation_id,
disposition,
@@ -784,12 +433,6 @@ fn local_restore_result(
}
}
-fn selected_config(config: &RuntimeConfig, selector: String) -> RuntimeConfig {
- let mut config = config.clone();
- config.account.selector = Some(selector);
- config
-}
-
fn publish_runtime_view(
config: &RuntimeConfig,
signed_write_required: bool,
@@ -955,18 +598,6 @@ fn health_status_state(store_state: &str, publish: &PublishRuntimeView) -> &'sta
}
}
-fn health_check_state(
- store_state: &str,
- account_ready: bool,
- publish: &PublishRuntimeView,
-) -> &'static str {
- if store_state == "ready" && account_ready && publish_runtime_ready(publish) {
- "ready"
- } else {
- "needs_attention"
- }
-}
-
fn publish_runtime_ready(publish: &PublishRuntimeView) -> bool {
!publish.signed_write_required || publish.executable
}
@@ -979,7 +610,7 @@ fn health_actions(
) -> Vec<String> {
let mut actions = Vec::new();
if store_state != "ready" {
- push_unique(&mut actions, "radroots store status get");
+ push_unique(&mut actions, "radroots store inspect");
}
if let Some(resolved) = account.resolved_account.as_ref() {
if !resolved.write_capable {
@@ -994,14 +625,6 @@ fn health_actions(
actions
}
-fn config_actions(
- config: &RuntimeConfig,
- account: &AccountResolution,
- publish: &PublishRuntimeView,
-) -> Vec<String> {
- publish_recovery_actions(config, account, publish)
-}
-
#[derive(Debug, Clone)]
struct AccountRemoveFarmOrphanWarning {
message: String,
@@ -1069,12 +692,12 @@ fn publish_recovery_actions(
if config.transport.nostr_relay_urls.is_empty() {
push_unique(
&mut actions,
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com",
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com",
);
}
if publish.signed_write_required {
if matches!(config.signer.backend, SignerBackend::Myc) {
- push_unique(&mut actions, "radroots signer status get");
+ push_unique(&mut actions, "radroots signer status");
} else if let Some(resolved) = account.resolved_account.as_ref() {
if !resolved.write_capable {
push_unique(&mut actions, "radroots account attach-secret");
@@ -1087,13 +710,13 @@ fn publish_recovery_actions(
TransportProfileKind::LocalOnly => {
push_unique(
&mut actions,
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com",
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com",
);
}
TransportProfileKind::Reticulum => {
push_unique(
&mut actions,
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com",
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com",
);
}
}
@@ -1169,300 +792,3 @@ fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
message,
}
}
-
-#[cfg(test)]
-mod tests {
- use radroots_identity::RadrootsIdentity;
- use radroots_secret_vault::RadrootsSecretBackend;
- use serde_json::{Map, Value};
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- use super::CoreOperationService;
- use crate::ops::{
- AccountAttachSecretRequest, AccountCreateRequest, AccountImportRequest, AccountListRequest,
- AccountRemoveRequest, OperationAdapter, OperationContext, OperationData, OperationRequest,
- StoreStatusGetRequest, WorkspaceGetRequest,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
- use crate::runtime::logging::LoggingState;
-
- #[test]
- fn core_service_envelopes_workspace_get() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let logging = LoggingState {
- initialized: true,
- current_file: None,
- };
- let service = OperationAdapter::new(CoreOperationService::new(&config, &logging));
- let request =
- OperationRequest::new(OperationContext::default(), WorkspaceGetRequest::default())
- .expect("workspace request");
- let result = service.execute(request).expect("workspace result");
- let envelope = result
- .to_envelope(OperationContext::default().envelope_context("req_workspace"))
- .expect("workspace envelope");
-
- assert_eq!(envelope.operation_id, "workspace.get");
- assert_eq!(envelope.kind, "workspace.get");
- assert_eq!(envelope.request_id, "req_workspace");
- assert_eq!(envelope.result["profile"], "interactive_user");
- assert_eq!(
- envelope.result["replica_store_path"],
- config.local.replica_store_path.display().to_string()
- );
- }
-
- #[test]
- fn core_service_backs_store_status() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let logging = LoggingState {
- initialized: false,
- current_file: None,
- };
- let service = OperationAdapter::new(CoreOperationService::new(&config, &logging));
- let request = OperationRequest::new(
- OperationContext::default(),
- StoreStatusGetRequest::default(),
- )
- .expect("store status request");
- let result = service.execute(request).expect("store status result");
- let envelope = result
- .to_envelope(OperationContext::default().envelope_context("req_store"))
- .expect("store status envelope");
-
- assert_eq!(envelope.operation_id, "store.status.get");
- assert_eq!(envelope.result["state"], "ready");
- assert_eq!(
- envelope.result["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(envelope.result["canonical_store"], "sdk");
- assert_eq!(envelope.result["sdk_storage"], "directory");
- assert_eq!(
- envelope.result["derived_projection"]["source"],
- "local derived projection cache"
- );
- assert_eq!(
- envelope.result["derived_projection"]["state"],
- "unconfigured"
- );
- assert_eq!(
- envelope.result["event_store"]["store"]["integrity_ok"],
- true
- );
- assert_eq!(envelope.result["outbox"]["store"]["integrity_ok"], true);
- }
-
- #[test]
- fn core_service_backs_account_create_and_list() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let logging = LoggingState {
- initialized: false,
- current_file: None,
- };
- let service = OperationAdapter::new(CoreOperationService::new(&config, &logging));
- let create =
- OperationRequest::new(OperationContext::default(), AccountCreateRequest::default())
- .expect("account create request");
- let create_result = service.execute(create).expect("account create result");
- let create_envelope = create_result
- .to_envelope(OperationContext::default().envelope_context("req_create"))
- .expect("account create envelope");
-
- assert_eq!(create_envelope.operation_id, "account.create");
- assert_eq!(create_envelope.result["state"], "created");
- assert!(create_envelope.result["account"]["id"].is_string());
-
- let list =
- OperationRequest::new(OperationContext::default(), AccountListRequest::default())
- .expect("account list request");
- let list_result = service.execute(list).expect("account list result");
- let list_envelope = list_result
- .to_envelope(OperationContext::default().envelope_context("req_list"))
- .expect("account list envelope");
-
- assert_eq!(list_envelope.operation_id, "account.list");
- assert_eq!(list_envelope.result["count"], 1);
- assert_eq!(list_envelope.result["accounts"][0]["is_default"], true);
- }
-
- #[test]
- fn account_create_ignores_configured_identity_path() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let configured_identity = RadrootsIdentity::generate();
- std::fs::create_dir_all(config.identity.path.parent().expect("identity parent"))
- .expect("identity parent dir");
- configured_identity
- .save_json(&config.identity.path)
- .expect("identity file");
- let logging = LoggingState {
- initialized: false,
- current_file: None,
- };
- let service = OperationAdapter::new(CoreOperationService::new(&config, &logging));
- let create =
- OperationRequest::new(OperationContext::default(), AccountCreateRequest::default())
- .expect("account create request");
- let create_result = service.execute(create).expect("account create result");
- let create_envelope = create_result
- .to_envelope(OperationContext::default().envelope_context("req_create"))
- .expect("account create envelope");
- let configured_identity_id = configured_identity.id().to_string();
-
- assert_eq!(create_envelope.result["state"], "created");
- assert_ne!(
- create_envelope.result["account"]["id"].as_str(),
- Some(configured_identity_id.as_str())
- );
- }
-
- #[test]
- fn core_required_account_approvals_return_approval_error() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let logging = LoggingState {
- initialized: false,
- current_file: None,
- };
- let service = OperationAdapter::new(CoreOperationService::new(&config, &logging));
- let import = OperationRequest::new(
- OperationContext::default(),
- AccountImportRequest::from_data(data(&[("path", "account.json")])),
- )
- .expect("account import request");
- let import_error = service.execute(import).expect_err("approval required");
- assert_eq!(import_error.to_output_error().code, "approval_required");
- assert_eq!(import_error.to_output_error().exit_code, 6);
-
- let attach_secret = OperationRequest::new(
- OperationContext::default(),
- AccountAttachSecretRequest::from_data(data(&[
- ("selector", "acct_test"),
- ("path", "account.json"),
- ])),
- )
- .expect("account attach-secret request");
- let attach_secret_error = service
- .execute(attach_secret)
- .expect_err("approval required");
- assert_eq!(
- attach_secret_error.to_output_error().code,
- "approval_required"
- );
- assert_eq!(attach_secret_error.to_output_error().exit_code, 6);
-
- let remove = OperationRequest::new(
- OperationContext::default(),
- AccountRemoveRequest::from_data(data(&[("selector", "acct_test")])),
- )
- .expect("account remove request");
- let remove_error = service.execute(remove).expect_err("approval required");
- assert_eq!(remove_error.to_output_error().code, "approval_required");
- assert_eq!(remove_error.to_output_error().exit_code, 6);
- }
-
- fn sample_config(root: &Path) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-
- fn data(entries: &[(&str, &str)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
- .collect::<Map<String, Value>>()
- }
-}
diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs
@@ -2,16 +2,11 @@ use serde::Serialize;
use serde_json::Value;
use crate::cli::global::{
- FarmCreateArgs, FarmFieldArg, FarmPrivateLocationKeyArgs, FarmPrivateLocationSetArgs,
- FarmPrivateLocationSetInput, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs,
- FarmUpdateArgs,
+ FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs,
};
use crate::ops::{
- FarmCreateRequest, FarmCreateResult, FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult,
- FarmGetRequest, FarmGetResult, FarmLocationClearRequest, FarmLocationClearResult,
- FarmLocationGetRequest, FarmLocationGetResult, FarmLocationSetRequest, FarmLocationSetResult,
- FarmProfileUpdateRequest, FarmProfileUpdateResult, FarmPublishRequest, FarmPublishResult,
- FarmReadinessCheckRequest, FarmReadinessCheckResult, FarmRebindRequest, FarmRebindResult,
+ FarmCreateRequest, FarmCreateResult, FarmGetRequest, FarmGetResult, FarmListRequest,
+ FarmListResult, FarmPublishRequest, FarmPublishResult, FarmUpdateRequest, FarmUpdateResult,
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
OperationResult, OperationResultData, OperationService,
};
@@ -82,132 +77,29 @@ impl OperationService<FarmGetRequest> for FarmOperationService<'_> {
}
}
-impl OperationService<FarmRebindRequest> for FarmOperationService<'_> {
- type Result = FarmRebindResult;
+impl OperationService<FarmUpdateRequest> for FarmOperationService<'_> {
+ type Result = FarmUpdateResult;
fn execute(
&self,
- request: OperationRequest<FarmRebindRequest>,
+ request: OperationRequest<FarmUpdateRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmRebindArgs {
- scope: scope_input(&request)?,
- selector: required_string(&request, "selector")?,
- };
- if request.context.dry_run {
- let view =
- crate::runtime::farm::rebind_preflight(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- return serialized_operation_result::<FarmRebindResult, _>(&view);
- }
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
-
- let view = crate::runtime::farm::rebind(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- serialized_operation_result::<FarmRebindResult, _>(&view)
+ farm_set::<FarmUpdateResult>(&request, self.config, profile_field(&request)?)
}
}
-impl OperationService<FarmProfileUpdateRequest> for FarmOperationService<'_> {
- type Result = FarmProfileUpdateResult;
+impl OperationService<FarmListRequest> for FarmOperationService<'_> {
+ type Result = FarmListResult;
fn execute(
&self,
- request: OperationRequest<FarmProfileUpdateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- farm_set::<FarmProfileUpdateResult>(&request, self.config, profile_field(&request)?)
- }
-}
-
-impl OperationService<FarmLocationSetRequest> for FarmOperationService<'_> {
- type Result = FarmLocationSetResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmLocationSetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmPrivateLocationSetArgs {
- farm_d_tag: string_input(&request, "farm_d_tag"),
- input: farm_private_location_input(&request)?,
- label: string_input(&request, "label"),
- };
- let view =
- crate::runtime::farm::private_location_set(self.config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- farm_private_location_set_result(request.operation_id(), &view)
- }
-}
-
-impl OperationService<FarmLocationGetRequest> for FarmOperationService<'_> {
- type Result = FarmLocationGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmLocationGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmPrivateLocationKeyArgs {
- farm_d_tag: string_input(&request, "farm_d_tag"),
- };
- let view =
- crate::runtime::farm::private_location_get(self.config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- serialized_operation_result::<FarmLocationGetResult, _>(&view)
- }
-}
-
-impl OperationService<FarmLocationClearRequest> for FarmOperationService<'_> {
- type Result = FarmLocationClearResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmLocationClearRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
- let args = FarmPrivateLocationKeyArgs {
- farm_d_tag: string_input(&request, "farm_d_tag"),
- };
- let view =
- crate::runtime::farm::private_location_clear(self.config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- serialized_operation_result::<FarmLocationClearResult, _>(&view)
- }
-}
-
-impl OperationService<FarmFulfillmentUpdateRequest> for FarmOperationService<'_> {
- type Result = FarmFulfillmentUpdateResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmFulfillmentUpdateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- farm_set::<FarmFulfillmentUpdateResult>(&request, self.config, FarmFieldArg::Delivery)
- }
-}
-
-impl OperationService<FarmReadinessCheckRequest> for FarmOperationService<'_> {
- type Result = FarmReadinessCheckResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmReadinessCheckRequest>,
+ request: OperationRequest<FarmListRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let args = FarmScopedArgs {
scope: scope_input(&request)?,
};
let view = map_runtime(crate::runtime::farm::status(self.config, &args))?;
- serialized_operation_result::<FarmReadinessCheckResult, _>(&view)
+ serialized_operation_result::<FarmListResult, _>(&view)
}
}
@@ -227,11 +119,6 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
.or_else(|| string_input(&request, "idempotency_key")),
print_event: bool_input(&request, "print_event").unwrap_or(false),
};
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
if matches!(
self.config.transport.profile,
TransportProfileKind::Nostr | TransportProfileKind::MultiTarget
@@ -247,7 +134,7 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
}
fn farm_set<R>(
- request: &OperationRequest<impl OperationRequestPayload + OperationRequestData>,
+ request: &OperationRequest<impl OperationRequestData>,
config: &RuntimeConfig,
field: FarmFieldArg,
) -> Result<OperationResult<R>, OperationAdapterError>
@@ -270,7 +157,7 @@ where
}
fn profile_field(
- request: &OperationRequest<impl OperationRequestPayload + OperationRequestData>,
+ request: &OperationRequest<impl OperationRequestData>,
) -> Result<FarmFieldArg, OperationAdapterError> {
match string_input(request, "field").as_deref() {
Some("name") | None => Ok(FarmFieldArg::Name),
@@ -327,29 +214,6 @@ fn farm_publish_result(
}
}
-fn farm_private_location_set_result(
- operation_id: &str,
- view: &crate::view::runtime::FarmPrivateLocationView,
-) -> Result<OperationResult<FarmLocationSetResult>, OperationAdapterError> {
- match view.state.as_str() {
- "no_match" => Err(OperationAdapterError::not_found_with_detail(
- operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "GeoNames lookup returned no matching locality".to_owned()),
- serde_json::to_value(view).unwrap_or(Value::Null),
- )),
- "ambiguous" => Err(OperationAdapterError::validation_failed_with_detail(
- operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "GeoNames lookup matched multiple localities".to_owned()),
- serde_json::to_value(view).unwrap_or(Value::Null),
- )),
- _ => serialized_operation_result::<FarmLocationSetResult, _>(view),
- }
-}
-
fn farm_publish_transport_delivery_unavailable(view: &FarmPublishView) -> bool {
view.state == "partial"
|| !view.profile.failed_transport_targets.is_empty()
@@ -424,154 +288,6 @@ where
.map(str::to_owned)
}
-fn farm_private_location_input<P>(
- request: &OperationRequest<P>,
-) -> Result<FarmPrivateLocationSetInput, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- if request.payload.input().contains_key("lookup") {
- return Err(invalid_input(
- request.operation_id(),
- "`lookup` is not a supported farm location input".to_owned(),
- ));
- }
-
- let lat = optional_f64(request, "lat")?;
- let lng = optional_f64(request, "lng")?;
- let city = trimmed_string_input(request, "city")?;
- let region = trimmed_string_input(request, "region")?;
- let country = trimmed_string_input(request, "country")?;
- let query = trimmed_string_input(request, "query")?;
- let geonames_id = optional_i64(request, "geonames_id")?;
-
- if (lat.is_some() || lng.is_some()) && !(lat.is_some() && lng.is_some()) {
- return Err(invalid_input(
- request.operation_id(),
- "`lat` and `lng` must be provided together".to_owned(),
- ));
- }
- if (region.is_some() || country.is_some()) && city.is_none() {
- return Err(invalid_input(
- request.operation_id(),
- "`region` and `country` require `city`".to_owned(),
- ));
- }
-
- let mode_count = usize::from(lat.is_some())
- + usize::from(city.is_some())
- + usize::from(query.is_some())
- + usize::from(geonames_id.is_some());
-
- if mode_count != 1 {
- return Err(invalid_input(
- request.operation_id(),
- "farm location requires exactly one of `lat`/`lng`, `city`, `query`, or `geonames_id`"
- .to_owned(),
- ));
- }
-
- if let (Some(latitude), Some(longitude)) = (lat, lng) {
- return Ok(FarmPrivateLocationSetInput::Exact {
- latitude,
- longitude,
- });
- }
- if let Some(city) = city {
- return Ok(FarmPrivateLocationSetInput::City {
- city,
- region,
- country,
- });
- }
- if let Some(query) = query {
- return Ok(FarmPrivateLocationSetInput::Query(query));
- }
- if let Some(geonames_id) = geonames_id {
- if geonames_id <= 0 {
- return Err(invalid_input(
- request.operation_id(),
- "`geonames_id` must be a positive integer".to_owned(),
- ));
- }
- return Ok(FarmPrivateLocationSetInput::GeonamesId(geonames_id));
- }
-
- Err(invalid_input(
- request.operation_id(),
- "farm location input could not be resolved".to_owned(),
- ))
-}
-
-fn optional_f64<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<Option<f64>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .map(|value| {
- value
- .as_f64()
- .filter(|value| value.is_finite())
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("`{key}` must be a finite number"),
- )
- })
- })
- .transpose()
-}
-
-fn optional_i64<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<Option<i64>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .map(|value| {
- value.as_i64().ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("`{key}` must be an integer"),
- )
- })
- })
- .transpose()
-}
-
-fn trimmed_string_input<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<Option<String>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key)
- .map(|value| {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- Err(invalid_input(
- request.operation_id(),
- format!("`{key}` must not be empty"),
- ))
- } else {
- Ok(trimmed.to_owned())
- }
- })
- .transpose()
-}
-
fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
where
P: OperationRequestPayload + OperationRequestData,
@@ -585,363 +301,3 @@ fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
message,
}
}
-
-#[cfg(test)]
-mod tests {
- use radroots_secret_vault::RadrootsSecretBackend;
- use serde_json::{Map, Value, json};
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- use super::FarmOperationService;
- use crate::ops::{
- FarmCreateRequest, FarmGetRequest, FarmLocationSetRequest, FarmPublishRequest,
- FarmReadinessCheckRequest, FarmRebindRequest, OperationAdapter, OperationContext,
- OperationData, OperationRequest,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
- use crate::view::runtime::{
- FarmPrivateExactLocationView, FarmPrivateLocationCandidateView, FarmPrivateLocationView,
- };
-
- #[test]
- fn farm_service_reports_missing_farm_config() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(FarmOperationService::new(&config));
- let request = OperationRequest::new(OperationContext::default(), FarmGetRequest::default())
- .expect("farm get request");
- let envelope = service
- .execute(request)
- .expect("farm get result")
- .to_envelope(OperationContext::default().envelope_context("req_farm_get"))
- .expect("farm get envelope");
-
- assert_eq!(envelope.operation_id, "farm.get");
- assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(envelope.result["config_present"], false);
- }
-
- #[test]
- fn farm_service_supports_create_and_readiness_dry_run() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(FarmOperationService::new(&config));
- let mut context = OperationContext::default();
- context.dry_run = true;
- let request = OperationRequest::new(
- context.clone(),
- FarmCreateRequest::from_data(data(&[("name", "dry farm"), ("location", "earth")])),
- )
- .expect("farm create request");
- let envelope = service
- .execute(request)
- .expect("farm create result")
- .to_envelope(context.envelope_context("req_farm_create"))
- .expect("farm create envelope");
-
- assert_eq!(envelope.operation_id, "farm.create");
- assert_eq!(envelope.dry_run, true);
- assert_eq!(envelope.result["state"], "unconfigured");
-
- let readiness = OperationRequest::new(
- OperationContext::default(),
- FarmReadinessCheckRequest::default(),
- )
- .expect("farm readiness request");
- let readiness_envelope = service
- .execute(readiness)
- .expect("farm readiness result")
- .to_envelope(OperationContext::default().envelope_context("req_farm_ready"))
- .expect("farm readiness envelope");
- assert_eq!(readiness_envelope.operation_id, "farm.readiness.check");
- assert_eq!(readiness_envelope.result["state"], "unconfigured");
- }
-
- #[test]
- fn farm_publish_requires_approval_token_unless_dry_run() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(FarmOperationService::new(&config));
- let request =
- OperationRequest::new(OperationContext::default(), FarmPublishRequest::default())
- .expect("farm publish request");
- let error = service.execute(request).expect_err("approval required");
- assert!(format!("{error}").contains("approval_token"));
- assert_eq!(error.to_output_error().code, "approval_required");
- assert_eq!(error.to_output_error().exit_code, 6);
- }
-
- #[test]
- fn farm_rebind_requires_approval_token_unless_dry_run() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(FarmOperationService::new(&config));
- let request = OperationRequest::new(
- OperationContext::default(),
- FarmRebindRequest::from_data(data(&[("selector", "acct_test")])),
- )
- .expect("farm rebind request");
- let error = service.execute(request).expect_err("approval required");
- assert!(format!("{error}").contains("approval_token"));
- assert_eq!(error.to_output_error().code, "approval_required");
- assert_eq!(error.to_output_error().exit_code, 6);
- }
-
- #[test]
- fn farm_service_accepts_canonical_location_set_modes() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(FarmOperationService::new(&config));
- let cases = [
- value_data(&[
- ("lat", json!(48.429456)),
- ("lng", json!(-123.349786)),
- ("label", json!("farm gate")),
- ]),
- value_data(&[
- ("city", json!("Victoria")),
- ("region", json!("BC")),
- ("country", json!("CA")),
- ]),
- value_data(&[("query", json!("Victoria, BC"))]),
- value_data(&[("geonames_id", json!(6174041))]),
- ];
-
- for input in cases {
- let request = OperationRequest::new(
- OperationContext::default(),
- FarmLocationSetRequest::from_data(input),
- )
- .expect("farm location request");
- let envelope = service
- .execute(request)
- .expect("farm location set result")
- .to_envelope(OperationContext::default().envelope_context("req_farm_location"))
- .expect("farm location envelope");
-
- assert_eq!(envelope.operation_id, "farm.location.set");
- assert_eq!(envelope.result["state"], "unconfigured");
- }
- }
-
- #[test]
- fn farm_service_rejects_invalid_location_set_modes() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(FarmOperationService::new(&config));
- let cases = [
- (
- value_data(&[("lookup", json!("Victoria, BC"))]),
- "`lookup` is not a supported farm location input",
- ),
- (
- value_data(&[("lat", json!(48.429456))]),
- "`lat` and `lng` must be provided together",
- ),
- (
- value_data(&[
- ("lat", json!(48.429456)),
- ("lng", json!(-123.349786)),
- ("city", json!("Victoria")),
- ]),
- "requires exactly one",
- ),
- (
- value_data(&[("query", json!("Victoria")), ("country", json!("CA"))]),
- "`region` and `country` require `city`",
- ),
- (
- value_data(&[("geonames_id", json!(0))]),
- "`geonames_id` must be a positive integer",
- ),
- (
- value_data(&[("city", json!(" "))]),
- "`city` must not be empty",
- ),
- ];
-
- for (input, expected) in cases {
- let request = OperationRequest::new(
- OperationContext::default(),
- FarmLocationSetRequest::from_data(input),
- )
- .expect("farm location request");
- let error = service
- .execute(request)
- .expect_err("invalid location input");
-
- assert!(format!("{error}").contains(expected));
- }
- }
-
- #[test]
- fn farm_location_set_maps_lookup_failures_to_output_errors() {
- let no_match = location_lookup_view("no_match", Vec::new());
- let no_match_error =
- super::farm_private_location_set_result("farm.location.set", &no_match)
- .expect_err("no match error")
- .to_output_error();
- assert_eq!(no_match_error.code, "not_found");
- assert_eq!(
- no_match_error.detail.as_ref().expect("no match detail")["state"],
- "no_match"
- );
-
- let ambiguous = location_lookup_view(
- "ambiguous",
- vec![FarmPrivateLocationCandidateView {
- geonames_feature_id: 3002,
- geonames_country_id: "CA".to_owned(),
- name: "Shared Market".to_owned(),
- display_name: "Shared Market, British Columbia, Canada".to_owned(),
- exact_location: FarmPrivateExactLocationView {
- lat: 48.7,
- lng: -123.2,
- },
- region: Some("British Columbia".to_owned()),
- country: Some("Canada".to_owned()),
- }],
- );
- let ambiguous_error =
- super::farm_private_location_set_result("farm.location.set", &ambiguous)
- .expect_err("ambiguous error")
- .to_output_error();
- assert_eq!(ambiguous_error.code, "validation_failed");
- assert_eq!(
- ambiguous_error.detail.as_ref().expect("ambiguous detail")["candidates"][0]["geonames_feature_id"],
- 3002
- );
- }
-
- fn sample_config(root: &Path) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-
- fn data(entries: &[(&str, &str)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
- .collect::<Map<String, Value>>()
- }
-
- fn value_data(entries: &[(&str, Value)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), value.clone()))
- .collect::<Map<String, Value>>()
- }
-
- fn location_lookup_view(
- state: &str,
- candidates: Vec<FarmPrivateLocationCandidateView>,
- ) -> FarmPrivateLocationView {
- FarmPrivateLocationView {
- state: state.to_owned(),
- source: "test".to_owned(),
- farm_addr: Some("30401:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAA".to_owned()),
- farm_d_tag: Some("AAAAAAAAAAAAAAAAAAAAAA".to_owned()),
- seller_account_id: Some("acct_test".to_owned()),
- seller_pubkey: Some("1111111111111111111111111111111111111111111111111111111111111111".to_owned()),
- label: None,
- exact_location: None,
- public_locality: None,
- geonames_feature_id: None,
- geonames_country_id: None,
- geonames_database_path: Some("cache/shared/geonames/geonames-1.0.db".to_owned()),
- cleared: None,
- candidates,
- reason: Some(format!("{state} reason")),
- actions: Vec::new(),
- }
- }
-}
diff --git a/src/ops/exec/listing.rs b/src/ops/exec/listing.rs
@@ -3,22 +3,18 @@ use std::path::PathBuf;
use serde::Serialize;
use serde_json::Value;
-use crate::cli::global::{
- ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
- ListingRebindArgs, RecordLookupArgs,
-};
+use crate::cli::global::{ListingCreateArgs, ListingMutationArgs, RecordLookupArgs};
use crate::ops::{
- ListingAppExportRequest, ListingAppExportResult, ListingAppListRequest, ListingAppListResult,
- ListingArchiveRequest, ListingArchiveResult, ListingCreateRequest, ListingCreateResult,
- ListingGetRequest, ListingGetResult, ListingListRequest, ListingListResult,
- ListingPublishRequest, ListingPublishResult, ListingRebindRequest, ListingRebindResult,
- ListingUpdateRequest, ListingUpdateResult, ListingValidateRequest, ListingValidateResult,
- OperationAdapterError, OperationNetworkMode, OperationRequest, OperationRequestData,
- OperationRequestPayload, OperationResult, OperationResultData, OperationService,
+ ListingCreateRequest, ListingCreateResult, ListingGetRequest, ListingGetResult,
+ ListingListRequest, ListingListResult, ListingPauseRequest, ListingPauseResult,
+ ListingPublishRequest, ListingPublishResult, ListingUpdateRequest, ListingUpdateResult,
+ ListingWithdrawRequest, ListingWithdrawResult, OperationAdapterError, OperationNetworkMode,
+ OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult,
+ OperationResultData, OperationService,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
-use crate::view::runtime::{CommandDisposition, ListingAppRecordExportView, ListingMutationView};
+use crate::view::runtime::{CommandDisposition, ListingMutationView};
pub struct ListingOperationService<'a> {
config: &'a RuntimeConfig,
@@ -108,44 +104,6 @@ impl OperationService<ListingListRequest> for ListingOperationService<'_> {
}
}
-impl OperationService<ListingAppListRequest> for ListingOperationService<'_> {
- type Result = ListingAppListResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingAppListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::app_record_list(self.config),
- )?;
- serialized_operation_result::<ListingAppListResult, _>(&view)
- }
-}
-
-impl OperationService<ListingAppExportRequest> for ListingOperationService<'_> {
- type Result = ListingAppExportResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingAppExportRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = ListingAppRecordExportArgs {
- record_id: required_string(&request, "record_id")?,
- output: optional_path(&request, "output"),
- };
- let mut config = self.config.clone();
- if request.context.dry_run {
- config.output.dry_run = true;
- }
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::app_record_export(&config, &args),
- )?;
- listing_app_record_export_result::<ListingAppExportResult>(request.operation_id(), &view)
- }
-}
-
impl OperationService<ListingUpdateRequest> for ListingOperationService<'_> {
type Result = ListingUpdateResult;
@@ -153,9 +111,6 @@ impl OperationService<ListingUpdateRequest> for ListingOperationService<'_> {
&self,
request: OperationRequest<ListingUpdateRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if !request.context.dry_run {
- require_approval(&request)?;
- }
let args = mutation_args(&request)?;
let config = mutation_config(self.config, &request);
let view = crate::runtime::listing::update(&config, &args).map_err(|error| {
@@ -165,87 +120,51 @@ impl OperationService<ListingUpdateRequest> for ListingOperationService<'_> {
}
}
-impl OperationService<ListingValidateRequest> for ListingOperationService<'_> {
- type Result = ListingValidateResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingValidateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = ListingFileArgs {
- file: required_path(&request, "file")?,
- };
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::validate(self.config, &args),
- )?;
- serialized_operation_result::<ListingValidateResult, _>(&view)
- }
-}
-
-impl OperationService<ListingRebindRequest> for ListingOperationService<'_> {
- type Result = ListingRebindResult;
+impl OperationService<ListingPublishRequest> for ListingOperationService<'_> {
+ type Result = ListingPublishResult;
fn execute(
&self,
- request: OperationRequest<ListingRebindRequest>,
+ request: OperationRequest<ListingPublishRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = ListingRebindArgs {
- file: required_path(&request, "file")?,
- selector: required_string(&request, "selector")?,
- farm_d_tag: string_input(&request, "farm_d_tag"),
- };
- if request.context.dry_run {
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::rebind_preflight(self.config, &args),
- )?;
- return serialized_operation_result::<ListingRebindResult, _>(&view);
- }
- require_approval(&request)?;
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::rebind(self.config, &args),
- )?;
- serialized_operation_result::<ListingRebindResult, _>(&view)
+ let args = mutation_args(&request)?;
+ let config = mutation_config(self.config, &request);
+ let view = crate::runtime::listing::publish_via_sdk(&config, &args).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
+ })?;
+ mutation_result::<ListingPublishResult>(request.operation_id(), &view)
}
}
-impl OperationService<ListingPublishRequest> for ListingOperationService<'_> {
- type Result = ListingPublishResult;
+impl OperationService<ListingPauseRequest> for ListingOperationService<'_> {
+ type Result = ListingPauseResult;
fn execute(
&self,
- request: OperationRequest<ListingPublishRequest>,
+ request: OperationRequest<ListingPauseRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if !request.context.dry_run {
- require_approval(&request)?;
- }
let args = mutation_args(&request)?;
let config = mutation_config(self.config, &request);
- let view = crate::runtime::listing::publish_via_sdk(&config, &args).map_err(|error| {
+ let view = crate::runtime::listing::pause(&config, &args).map_err(|error| {
OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
})?;
- mutation_result::<ListingPublishResult>(request.operation_id(), &view)
+ mutation_result::<ListingPauseResult>(request.operation_id(), &view)
}
}
-impl OperationService<ListingArchiveRequest> for ListingOperationService<'_> {
- type Result = ListingArchiveResult;
+impl OperationService<ListingWithdrawRequest> for ListingOperationService<'_> {
+ type Result = ListingWithdrawResult;
fn execute(
&self,
- request: OperationRequest<ListingArchiveRequest>,
+ request: OperationRequest<ListingWithdrawRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if !request.context.dry_run {
- require_approval(&request)?;
- }
let args = mutation_args(&request)?;
let config = mutation_config(self.config, &request);
- let view = crate::runtime::listing::archive(&config, &args).map_err(|error| {
+ let view = crate::runtime::listing::withdraw(&config, &args).map_err(|error| {
OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
})?;
- mutation_result::<ListingArchiveResult>(request.operation_id(), &view)
+ mutation_result::<ListingWithdrawResult>(request.operation_id(), &view)
}
}
@@ -278,18 +197,6 @@ where
})
}
-fn require_approval<P>(request: &OperationRequest<P>) -> Result<(), OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
- Ok(())
-}
-
fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
where
R: OperationResultData,
@@ -345,50 +252,6 @@ fn listing_transport_delivery_unavailable(view: &ListingMutationView) -> bool {
|| !view.failed_transport_targets.is_empty())
}
-fn listing_app_record_export_result<R>(
- operation_id: &str,
- view: &ListingAppRecordExportView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<R, _>(view),
- CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "app-authored local record `{}` was not found",
- view.record_id
- )
- }),
- serde_json::to_value(view).unwrap_or(Value::Null),
- )),
- CommandDisposition::ValidationFailed => {
- Err(OperationAdapterError::validation_failed_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "app-authored local record `{}` cannot be exported",
- view.record_id
- )
- }),
- serde_json::to_value(view).unwrap_or(Value::Null),
- ))
- }
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "app-authored local record export finished with state `{}`",
- view.state
- )
- }),
- )),
- }
-}
-
fn map_runtime<T>(
operation_id: &str,
result: Result<T, RuntimeError>,
@@ -468,7 +331,7 @@ mod tests {
use super::ListingOperationService;
use crate::ops::{
- ListingArchiveRequest, ListingCreateRequest, ListingListRequest, ListingPublishRequest,
+ ListingCreateRequest, ListingListRequest, ListingPublishRequest, ListingWithdrawRequest,
OperationAdapter, OperationContext, OperationData, OperationRequest,
};
use crate::runtime::config::{
@@ -482,8 +345,10 @@ mod tests {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
let service = OperationAdapter::new(ListingOperationService::new(&config));
- let mut context = OperationContext::default();
- context.dry_run = true;
+ let context = OperationContext {
+ dry_run: true,
+ ..Default::default()
+ };
let request = OperationRequest::new(
context.clone(),
ListingCreateRequest::from_data(data(&[("key", "eggs"), ("title", "Eggs")])),
@@ -518,29 +383,30 @@ mod tests {
}
#[test]
- fn listing_publish_and_archive_require_approval_unless_dry_run() {
+ fn listing_publish_and_withdraw_errors_do_not_use_retired_approval_language() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
let service = OperationAdapter::new(ListingOperationService::new(&config));
+ let retired_approval_language = ["approval", "token"].join("_");
let publish = OperationRequest::new(
OperationContext::default(),
ListingPublishRequest::from_data(data(&[("file", "listing.toml")])),
)
.expect("listing publish request");
- let publish_error = service.execute(publish).expect_err("approval required");
- assert!(format!("{publish_error}").contains("approval_token"));
- assert_eq!(publish_error.to_output_error().code, "approval_required");
- assert_eq!(publish_error.to_output_error().exit_code, 6);
-
- let mut context = OperationContext::default();
- context.dry_run = true;
- let archive = OperationRequest::new(
+ let publish_error = service.execute(publish).expect_err("publish preflight");
+ assert!(!format!("{publish_error}").contains(retired_approval_language.as_str()));
+
+ let context = OperationContext {
+ dry_run: true,
+ ..Default::default()
+ };
+ let withdraw = OperationRequest::new(
context.clone(),
- ListingArchiveRequest::from_data(data(&[("file", "listing.toml")])),
+ ListingWithdrawRequest::from_data(data(&[("file", "listing.toml")])),
)
- .expect("listing archive request");
- let archive_error = service.execute(archive).expect_err("archive preflight");
- assert!(!format!("{archive_error}").contains("approval_token"));
+ .expect("listing withdraw request");
+ let withdraw_error = service.execute(withdraw).expect_err("withdraw preflight");
+ assert!(!format!("{withdraw_error}").contains(retired_approval_language.as_str()));
}
fn sample_config(root: &Path) -> RuntimeConfig {
diff --git a/src/ops/exec/market.rs b/src/ops/exec/market.rs
@@ -3,10 +3,9 @@ use serde_json::Value;
use crate::cli::global::{FindQueryArgs, RecordLookupArgs};
use crate::ops::{
- MarketListingGetRequest, MarketListingGetResult, MarketProductSearchRequest,
- MarketProductSearchResult, MarketRefreshRequest, MarketRefreshResult, OperationAdapterError,
- OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult,
- OperationResultData, OperationService,
+ MarketGetRequest, MarketGetResult, MarketPullRequest, MarketPullResult, MarketSearchRequest,
+ MarketSearchResult, OperationAdapterError, OperationRequest, OperationRequestData,
+ OperationRequestPayload, OperationResult, OperationResultData, OperationService,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
@@ -22,26 +21,26 @@ impl<'a> MarketOperationService<'a> {
}
}
-impl OperationService<MarketRefreshRequest> for MarketOperationService<'_> {
- type Result = MarketRefreshResult;
+impl OperationService<MarketPullRequest> for MarketOperationService<'_> {
+ type Result = MarketPullResult;
fn execute(
&self,
- _request: OperationRequest<MarketRefreshRequest>,
+ _request: OperationRequest<MarketPullRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let view = market_refresh_view(map_runtime(crate::runtime::sync::market_refresh(
self.config,
))?);
- serialized_operation_result::<MarketRefreshResult, _>(&view)
+ serialized_operation_result::<MarketPullResult, _>(&view)
}
}
-impl OperationService<MarketProductSearchRequest> for MarketOperationService<'_> {
- type Result = MarketProductSearchResult;
+impl OperationService<MarketSearchRequest> for MarketOperationService<'_> {
+ type Result = MarketSearchResult;
fn execute(
&self,
- request: OperationRequest<MarketProductSearchRequest>,
+ request: OperationRequest<MarketSearchRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let args = FindQueryArgs {
query: required_query_terms(&request)?,
@@ -50,16 +49,16 @@ impl OperationService<MarketProductSearchRequest> for MarketOperationService<'_>
self.config,
&args,
))?);
- serialized_operation_result::<MarketProductSearchResult, _>(&view)
+ serialized_operation_result::<MarketSearchResult, _>(&view)
}
}
-impl OperationService<MarketListingGetRequest> for MarketOperationService<'_> {
- type Result = MarketListingGetResult;
+impl OperationService<MarketGetRequest> for MarketOperationService<'_> {
+ type Result = MarketGetResult;
fn execute(
&self,
- request: OperationRequest<MarketListingGetRequest>,
+ request: OperationRequest<MarketGetRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let args = RecordLookupArgs {
key: required_lookup(&request)?,
@@ -68,21 +67,21 @@ impl OperationService<MarketListingGetRequest> for MarketOperationService<'_> {
self.config,
&args,
))?);
- serialized_operation_result::<MarketListingGetResult, _>(&view)
+ serialized_operation_result::<MarketGetResult, _>(&view)
}
}
fn market_refresh_view(mut view: SyncActionView) -> SyncActionView {
view.actions = match view.state.as_str() {
- "ready" => vec!["radroots market product search tomatoes".to_owned()],
- "unavailable" => vec!["radroots sync status get".to_owned()],
+ "ready" => vec!["radroots market search tomatoes".to_owned()],
+ "unavailable" => vec!["radroots sync status".to_owned()],
"unconfigured" => {
let mut actions = Vec::new();
if view.replica_store == "missing" {
- actions.push("radroots store init".to_owned());
+ actions.push("radroots store inspect".to_owned());
}
if view.configured_transport_target_count == 0 {
- actions.push("radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com".to_owned());
+ actions.push("radroots transport config update --kind nostr --nostr-relay wss://relay.example.com".to_owned());
}
if actions.is_empty() {
actions.extend(std::mem::take(&mut view.actions));
@@ -100,10 +99,7 @@ fn market_product_search_view(mut view: FindView) -> FindView {
.results
.first()
.map(|result| {
- let mut actions = vec![format!(
- "radroots market listing get {}",
- result.product_key
- )];
+ let mut actions = vec![format!("radroots market get {}", result.product_key)];
if result.readiness.checkout_enabled {
actions.push("radroots basket create".to_owned());
}
@@ -111,12 +107,12 @@ fn market_product_search_view(mut view: FindView) -> FindView {
})
.unwrap_or_default(),
"empty" => vec![
- "radroots market refresh".to_owned(),
- "radroots market product search eggs".to_owned(),
+ "radroots market pull".to_owned(),
+ "radroots market search eggs".to_owned(),
],
"unconfigured" => vec![
- "radroots store init".to_owned(),
- "radroots market refresh".to_owned(),
+ "radroots store inspect".to_owned(),
+ "radroots market pull".to_owned(),
],
_ => std::mem::take(&mut view.actions),
};
@@ -133,12 +129,12 @@ fn market_listing_get_view(mut view: ListingGetView) -> ListingGetView {
}
}
"missing" => vec![
- "radroots market product search tomatoes".to_owned(),
- "radroots market refresh".to_owned(),
+ "radroots market search tomatoes".to_owned(),
+ "radroots market pull".to_owned(),
],
"unconfigured" => vec![
- "radroots store init".to_owned(),
- "radroots market refresh".to_owned(),
+ "radroots store inspect".to_owned(),
+ "radroots market pull".to_owned(),
],
_ => std::mem::take(&mut view.actions),
};
@@ -248,8 +244,8 @@ mod tests {
use super::{MarketOperationService, market_listing_get_view, market_product_search_view};
use crate::ops::{
- MarketListingGetRequest, MarketProductSearchRequest, MarketRefreshRequest,
- OperationAdapter, OperationContext, OperationData, OperationRequest,
+ MarketGetRequest, MarketPullRequest, MarketSearchRequest, OperationAdapter,
+ OperationContext, OperationData, OperationRequest,
};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
@@ -269,7 +265,7 @@ mod tests {
let config = sample_config(dir.path());
let service = OperationAdapter::new(MarketOperationService::new(&config));
let request =
- OperationRequest::new(OperationContext::default(), MarketRefreshRequest::default())
+ OperationRequest::new(OperationContext::default(), MarketPullRequest::default())
.expect("market refresh request");
let envelope = service
.execute(request)
@@ -277,10 +273,10 @@ mod tests {
.to_envelope(OperationContext::default().envelope_context("req_market_refresh"))
.expect("market refresh envelope");
- assert_eq!(envelope.operation_id, "market.refresh");
+ assert_eq!(envelope.operation_id, "market.pull");
assert_eq!(envelope.result["state"], "unconfigured");
assert_eq!(envelope.result["direction"], "pull");
- assert_eq!(envelope.result["actions"][0], "radroots store init");
+ assert_eq!(envelope.result["actions"][0], "radroots store inspect");
}
#[test]
@@ -288,9 +284,11 @@ mod tests {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
let service = OperationAdapter::new(MarketOperationService::new(&config));
- let mut context = OperationContext::default();
- context.dry_run = true;
- let request = OperationRequest::new(context.clone(), MarketRefreshRequest::default())
+ let context = OperationContext {
+ dry_run: true,
+ ..Default::default()
+ };
+ let request = OperationRequest::new(context.clone(), MarketPullRequest::default())
.expect("market refresh request");
let envelope = service
.execute(request)
@@ -298,8 +296,8 @@ mod tests {
.to_envelope(context.envelope_context("req_market_refresh"))
.expect("market refresh envelope");
- assert_eq!(envelope.operation_id, "market.refresh");
- assert_eq!(envelope.dry_run, true);
+ assert_eq!(envelope.operation_id, "market.pull");
+ assert!(envelope.dry_run);
assert_eq!(envelope.result["state"], "unconfigured");
assert_eq!(envelope.result["replica_store"], "missing");
assert_eq!(envelope.result["direction"], "pull");
@@ -314,9 +312,11 @@ mod tests {
crate::runtime::store::init(&config).expect("store init");
let service = OperationAdapter::new(MarketOperationService::new(&config));
- let mut context = OperationContext::default();
- context.dry_run = true;
- let request = OperationRequest::new(context.clone(), MarketRefreshRequest::default())
+ let context = OperationContext {
+ dry_run: true,
+ ..Default::default()
+ };
+ let request = OperationRequest::new(context.clone(), MarketPullRequest::default())
.expect("market refresh request");
let envelope = service
.execute(request)
@@ -324,7 +324,7 @@ mod tests {
.to_envelope(context.envelope_context("req_market_refresh"))
.expect("market refresh envelope");
- assert_eq!(envelope.operation_id, "market.refresh");
+ assert_eq!(envelope.operation_id, "market.pull");
assert_eq!(envelope.result["state"], "ready");
assert_eq!(
envelope.result["target_transport_endpoints"][0],
@@ -349,7 +349,7 @@ mod tests {
crate::runtime::store::init(&config).expect("store init");
let service = OperationAdapter::new(MarketOperationService::new(&config));
let request =
- OperationRequest::new(OperationContext::default(), MarketRefreshRequest::default())
+ OperationRequest::new(OperationContext::default(), MarketPullRequest::default())
.expect("market refresh request");
let envelope = service
.execute(request)
@@ -360,7 +360,7 @@ mod tests {
assert_eq!(envelope.result["state"], "unconfigured");
assert_eq!(
envelope.result["actions"][0],
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
);
}
@@ -371,7 +371,7 @@ mod tests {
let service = OperationAdapter::new(MarketOperationService::new(&config));
let request = OperationRequest::new(
OperationContext::default(),
- MarketProductSearchRequest::from_data(data(&[("query", "eggs")])),
+ MarketSearchRequest::from_data(data(&[("query", "eggs")])),
)
.expect("market product search request");
let envelope = service
@@ -380,10 +380,10 @@ mod tests {
.to_envelope(OperationContext::default().envelope_context("req_market_search"))
.expect("market product search envelope");
- assert_eq!(envelope.operation_id, "market.product.search");
+ assert_eq!(envelope.operation_id, "market.search");
assert_eq!(envelope.result["state"], "unconfigured");
assert_eq!(envelope.result["query"], "eggs");
- assert_eq!(envelope.result["actions"][0], "radroots store init");
+ assert_eq!(envelope.result["actions"][0], "radroots store inspect");
}
#[test]
@@ -391,11 +391,9 @@ mod tests {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
let service = OperationAdapter::new(MarketOperationService::new(&config));
- let request = OperationRequest::new(
- OperationContext::default(),
- MarketListingGetRequest::default(),
- )
- .expect("market listing get request");
+ let request =
+ OperationRequest::new(OperationContext::default(), MarketGetRequest::default())
+ .expect("market listing get request");
let error = service.execute(request).expect_err("key required");
assert!(format!("{error}").contains("`key`"));
@@ -408,7 +406,7 @@ mod tests {
let service = OperationAdapter::new(MarketOperationService::new(&config));
let request = OperationRequest::new(
OperationContext::default(),
- MarketListingGetRequest::from_data(data(&[("key", "eggs")])),
+ MarketGetRequest::from_data(data(&[("key", "eggs")])),
)
.expect("market listing get request");
let envelope = service
@@ -417,9 +415,9 @@ mod tests {
.to_envelope(OperationContext::default().envelope_context("req_market_listing"))
.expect("market listing get envelope");
- assert_eq!(envelope.operation_id, "market.listing.get");
+ assert_eq!(envelope.operation_id, "market.get");
assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(envelope.result["actions"][0], "radroots store init");
+ assert_eq!(envelope.result["actions"][0], "radroots store inspect");
}
#[test]
@@ -455,7 +453,7 @@ mod tests {
assert_eq!(
search.actions,
vec![
- "radroots market listing get eggs".to_owned(),
+ "radroots market get eggs".to_owned(),
"radroots basket create".to_owned()
]
);
@@ -522,7 +520,7 @@ mod tests {
assert_eq!(
disabled_search.actions,
- vec!["radroots market listing get eggs".to_owned()]
+ vec!["radroots market get eggs".to_owned()]
);
let disabled_listing = market_listing_get_view(ListingGetView {
diff --git a/src/ops/exec/runtime.rs b/src/ops/exec/runtime.rs
@@ -1,18 +1,15 @@
use serde::Serialize;
-use serde_json::Value;
+use serde_json::{Value, json};
-use crate::cli::global::SyncWatchArgs;
use crate::ops::{
- MeshPolicyCheckRequest, MeshPolicyCheckResult, MeshScopeGetRequest, MeshScopeGetResult,
- MeshScopeSetRequest, MeshScopeSetResult, MeshStatusRequest, MeshStatusResult,
- OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
- OperationResult, OperationResultData, OperationService, SignerStatusGetRequest,
- SignerStatusGetResult, SyncPullRequest, SyncPullResult, SyncPushRequest, SyncPushResult,
- SyncStatusGetRequest, SyncStatusGetResult, SyncWatchRequest, SyncWatchResult,
- TransportOutboxPushRequest, TransportOutboxPushResult, TransportOutboxStatusRequest,
- TransportOutboxStatusResult, TransportProfileGetRequest, TransportProfileGetResult,
- TransportProfileSetRequest, TransportProfileSetResult, TransportStatusRequest,
- TransportStatusResult,
+ DiagnosticsInspectRequest, DiagnosticsInspectResult, OperationAdapterError, OperationRequest,
+ OperationRequestData, OperationResult, OperationResultData, OperationService,
+ SignerStatusRequest, SignerStatusResult, SyncPullRequest, SyncPullResult, SyncPushRequest,
+ SyncPushResult, SyncStatusRequest, SyncStatusResult, TransportCapabilityListRequest,
+ TransportCapabilityListResult, TransportConfigInspectRequest, TransportConfigInspectResult,
+ TransportConfigUpdateRequest, TransportConfigUpdateResult, TransportDeliveryInspectRequest,
+ TransportDeliveryInspectResult, TransportDeliveryRetryRequest, TransportDeliveryRetryResult,
+ TransportStatusInspectRequest, TransportStatusInspectResult,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
@@ -28,151 +25,112 @@ impl<'a> RuntimeOperationService<'a> {
}
}
-impl OperationService<SignerStatusGetRequest> for RuntimeOperationService<'_> {
- type Result = SignerStatusGetResult;
+impl OperationService<SignerStatusRequest> for RuntimeOperationService<'_> {
+ type Result = SignerStatusResult;
fn execute(
&self,
- _request: OperationRequest<SignerStatusGetRequest>,
+ _request: OperationRequest<SignerStatusRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let view = crate::runtime::signer::resolve_signer_status(self.config);
- serialized_operation_result::<SignerStatusGetResult, _>(&view)
+ serialized_operation_result::<SignerStatusResult, _>(&view)
}
}
-impl OperationService<TransportProfileGetRequest> for RuntimeOperationService<'_> {
- type Result = TransportProfileGetResult;
+impl OperationService<TransportConfigInspectRequest> for RuntimeOperationService<'_> {
+ type Result = TransportConfigInspectResult;
fn execute(
&self,
- _request: OperationRequest<TransportProfileGetRequest>,
+ _request: OperationRequest<TransportConfigInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let view = crate::runtime::transport::profile(self.config);
- serialized_operation_result::<TransportProfileGetResult, _>(&view)
+ serialized_operation_result::<TransportConfigInspectResult, _>(&view)
}
}
-impl OperationService<TransportProfileSetRequest> for RuntimeOperationService<'_> {
- type Result = TransportProfileSetResult;
+impl OperationService<TransportConfigUpdateRequest> for RuntimeOperationService<'_> {
+ type Result = TransportConfigUpdateResult;
fn execute(
&self,
- request: OperationRequest<TransportProfileSetRequest>,
+ request: OperationRequest<TransportConfigUpdateRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let view = map_runtime(
- "transport.profile.set",
+ request.operation_id(),
crate::runtime::transport::set_profile(self.config, request.payload.input()),
)?;
- serialized_operation_result::<TransportProfileSetResult, _>(&view)
+ serialized_operation_result::<TransportConfigUpdateResult, _>(&view)
}
}
-impl OperationService<TransportStatusRequest> for RuntimeOperationService<'_> {
- type Result = TransportStatusResult;
+impl OperationService<TransportCapabilityListRequest> for RuntimeOperationService<'_> {
+ type Result = TransportCapabilityListResult;
fn execute(
&self,
- _request: OperationRequest<TransportStatusRequest>,
+ _request: OperationRequest<TransportCapabilityListRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::transport::status(self.config);
- serialized_operation_result::<TransportStatusResult, _>(&view)
+ let status = crate::runtime::transport::status(self.config);
+ let view = json!({
+ "state": "ready",
+ "source": "Runtime Contract V1 transport capability registry",
+ "active_profile": status.active_profile,
+ "transports": status.transports,
+ });
+ serialized_operation_result::<TransportCapabilityListResult, _>(&view)
}
}
-impl OperationService<TransportOutboxStatusRequest> for RuntimeOperationService<'_> {
- type Result = TransportOutboxStatusResult;
+impl OperationService<TransportStatusInspectRequest> for RuntimeOperationService<'_> {
+ type Result = TransportStatusInspectResult;
fn execute(
&self,
- _request: OperationRequest<TransportOutboxStatusRequest>,
+ _request: OperationRequest<TransportStatusInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::transport::outbox_status(self.config).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure("transport.outbox.status", error)
- })?;
- serialized_operation_result::<TransportOutboxStatusResult, _>(&view)
+ let view = crate::runtime::transport::status(self.config);
+ serialized_operation_result::<TransportStatusInspectResult, _>(&view)
}
}
-impl OperationService<TransportOutboxPushRequest> for RuntimeOperationService<'_> {
- type Result = TransportOutboxPushResult;
+impl OperationService<TransportDeliveryInspectRequest> for RuntimeOperationService<'_> {
+ type Result = TransportDeliveryInspectResult;
fn execute(
&self,
- request: OperationRequest<TransportOutboxPushRequest>,
+ _request: OperationRequest<TransportDeliveryInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- "transport.outbox.push",
- ));
- }
- let view = crate::runtime::transport::outbox_push(self.config).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure("transport.outbox.push", error)
+ let view = crate::runtime::transport::outbox_status(self.config).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure("transport.delivery.inspect", error)
})?;
- serialized_operation_result::<TransportOutboxPushResult, _>(&view)
- }
-}
-
-impl OperationService<MeshScopeGetRequest> for RuntimeOperationService<'_> {
- type Result = MeshScopeGetResult;
-
- fn execute(
- &self,
- _request: OperationRequest<MeshScopeGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::mesh::scope(self.config);
- serialized_operation_result::<MeshScopeGetResult, _>(&view)
- }
-}
-
-impl OperationService<MeshScopeSetRequest> for RuntimeOperationService<'_> {
- type Result = MeshScopeSetResult;
-
- fn execute(
- &self,
- request: OperationRequest<MeshScopeSetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_runtime(
- "mesh.scope.set",
- crate::runtime::mesh::set_scope(self.config, request.payload.input()),
- )?;
- serialized_operation_result::<MeshScopeSetResult, _>(&view)
+ serialized_operation_result::<TransportDeliveryInspectResult, _>(&view)
}
}
-impl OperationService<MeshStatusRequest> for RuntimeOperationService<'_> {
- type Result = MeshStatusResult;
+impl OperationService<TransportDeliveryRetryRequest> for RuntimeOperationService<'_> {
+ type Result = TransportDeliveryRetryResult;
fn execute(
&self,
- _request: OperationRequest<MeshStatusRequest>,
+ request: OperationRequest<TransportDeliveryRetryRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::mesh::status(self.config);
- serialized_operation_result::<MeshStatusResult, _>(&view)
- }
-}
-
-impl OperationService<MeshPolicyCheckRequest> for RuntimeOperationService<'_> {
- type Result = MeshPolicyCheckResult;
-
- fn execute(
- &self,
- _request: OperationRequest<MeshPolicyCheckRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::mesh::policy_check(self.config);
- serialized_operation_result::<MeshPolicyCheckResult, _>(&view)
+ let view = crate::runtime::transport::outbox_push(self.config).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
+ })?;
+ serialized_operation_result::<TransportDeliveryRetryResult, _>(&view)
}
}
-impl OperationService<SyncStatusGetRequest> for RuntimeOperationService<'_> {
- type Result = SyncStatusGetResult;
+impl OperationService<SyncStatusRequest> for RuntimeOperationService<'_> {
+ type Result = SyncStatusResult;
fn execute(
&self,
- _request: OperationRequest<SyncStatusGetRequest>,
+ _request: OperationRequest<SyncStatusRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::sync::status(self.config).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure("sync.status.get", error)
- })?;
+ let view = crate::runtime::sync::status(self.config)
+ .map_err(|error| OperationAdapterError::sdk_adapter_failure("sync.status", error))?;
sync_status_result(&view)
}
}
@@ -194,33 +152,43 @@ impl OperationService<SyncPushRequest> for RuntimeOperationService<'_> {
fn execute(
&self,
- request: OperationRequest<SyncPushRequest>,
+ _request: OperationRequest<SyncPushRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required("sync.push"));
- }
let view = crate::runtime::sync::push(self.config)
.map_err(|error| OperationAdapterError::sdk_adapter_failure("sync.push", error))?;
sync_action_result::<SyncPushResult>("sync.push", &view)
}
}
-impl OperationService<SyncWatchRequest> for RuntimeOperationService<'_> {
- type Result = SyncWatchResult;
+impl OperationService<DiagnosticsInspectRequest> for RuntimeOperationService<'_> {
+ type Result = DiagnosticsInspectResult;
fn execute(
&self,
- request: OperationRequest<SyncWatchRequest>,
+ _request: OperationRequest<DiagnosticsInspectRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = SyncWatchArgs {
- frames: usize_input(&request, "frames").unwrap_or(1),
- interval_ms: u64_input(&request, "interval_ms").unwrap_or(1_000),
+ let signer = crate::runtime::signer::resolve_signer_status(self.config);
+ let transport = crate::runtime::transport::status(self.config);
+ let sync = match crate::runtime::sync::status(self.config) {
+ Ok(view) => serde_json::to_value(view).unwrap_or_else(|_| json!({ "state": "error" })),
+ Err(error) => json!({
+ "state": "unavailable",
+ "reason": error.to_string(),
+ }),
};
- let view = map_runtime(
- "sync.watch",
- crate::runtime::sync::watch(self.config, &args),
- )?;
- serialized_operation_result::<SyncWatchResult, _>(&view)
+ let state = if sync.get("state").and_then(Value::as_str) == Some("unavailable") {
+ "degraded"
+ } else {
+ "ready"
+ };
+ let view = json!({
+ "state": state,
+ "source": "Runtime Contract V1 diagnostics",
+ "signer": signer,
+ "transport": transport,
+ "sync": sync,
+ });
+ serialized_operation_result::<DiagnosticsInspectResult, _>(&view)
}
}
@@ -234,11 +202,11 @@ where
fn sync_status_result(
view: &SyncStatusView,
-) -> Result<OperationResult<SyncStatusGetResult>, OperationAdapterError> {
+) -> Result<OperationResult<SyncStatusResult>, OperationAdapterError> {
match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<SyncStatusGetResult, _>(view),
+ CommandDisposition::Success => serialized_operation_result::<SyncStatusResult, _>(view),
disposition => Err(sync_view_error(
- "sync.status.get",
+ "sync.status",
disposition,
view,
view.reason.as_deref(),
@@ -309,25 +277,6 @@ fn map_runtime<T>(
result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error))
}
-fn usize_input<P>(request: &OperationRequest<P>, key: &str) -> Option<usize>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_u64)
- .and_then(|value| usize::try_from(value).ok())
-}
-
-fn u64_input<P>(request: &OperationRequest<P>, key: &str) -> Option<u64>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request.payload.input().get(key).and_then(Value::as_u64)
-}
-
#[cfg(test)]
mod tests {
use radroots_secret_vault::RadrootsSecretBackend;
@@ -336,8 +285,8 @@ mod tests {
use super::RuntimeOperationService;
use crate::ops::{
- OperationAdapter, OperationContext, OperationRequest, SignerStatusGetRequest,
- SyncStatusGetRequest, TransportProfileGetRequest,
+ OperationAdapter, OperationContext, OperationRequest, SignerStatusRequest,
+ SyncStatusRequest, TransportConfigInspectRequest,
};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
@@ -351,22 +300,20 @@ mod tests {
let config = sample_config(dir.path(), vec!["wss://relay.test".into()]);
let service = OperationAdapter::new(RuntimeOperationService::new(&config));
- let signer = OperationRequest::new(
- OperationContext::default(),
- SignerStatusGetRequest::default(),
- )
- .expect("signer status request");
+ let signer =
+ OperationRequest::new(OperationContext::default(), SignerStatusRequest::default())
+ .expect("signer status request");
let signer_envelope = service
.execute(signer)
.expect("signer status result")
.to_envelope(OperationContext::default().envelope_context("req_signer"))
.expect("signer envelope");
- assert_eq!(signer_envelope.operation_id, "signer.status.get");
+ assert_eq!(signer_envelope.operation_id, "signer.status");
assert_eq!(signer_envelope.result["state"], "unconfigured");
let profile = OperationRequest::new(
OperationContext::default(),
- TransportProfileGetRequest::default(),
+ TransportConfigInspectRequest::default(),
)
.expect("transport profile request");
let profile_envelope = service
@@ -374,7 +321,7 @@ mod tests {
.expect("transport profile result")
.to_envelope(OperationContext::default().envelope_context("req_transport"))
.expect("transport profile envelope");
- assert_eq!(profile_envelope.operation_id, "transport.profile.get");
+ assert_eq!(profile_envelope.operation_id, "transport.config.inspect");
assert_eq!(profile_envelope.result["state"], "configured");
assert_eq!(profile_envelope.result["profile_id"], "nostr");
}
@@ -385,16 +332,15 @@ mod tests {
let config = sample_config(dir.path(), Vec::new());
let service = OperationAdapter::new(RuntimeOperationService::new(&config));
- let sync =
- OperationRequest::new(OperationContext::default(), SyncStatusGetRequest::default())
- .expect("sync status request");
+ let sync = OperationRequest::new(OperationContext::default(), SyncStatusRequest::default())
+ .expect("sync status request");
let envelope = service
.execute(sync)
.expect("sync status result")
.to_envelope(OperationContext::default().envelope_context("req_sync_status"))
.expect("sync status envelope");
- assert_eq!(envelope.operation_id, "sync.status.get");
+ assert_eq!(envelope.operation_id, "sync.status");
assert_eq!(envelope.result["state"], "ready");
assert_eq!(
envelope.result["source"],
diff --git a/src/ops/exec/trade.rs b/src/ops/exec/trade.rs
@@ -1,31 +1,22 @@
-use std::path::PathBuf;
-
use serde::Serialize;
use serde_json::{Value, json};
use crate::cli::global::{
- RecordLookupArgs, TradeAppRecordExportArgs, TradeCancelArgs, TradeDecisionArg,
- TradeDecisionArgs, TradeRebindArgs, TradeStatusArgs, TradeSubmitArgs,
+ RecordLookupArgs, TradeCancelArgs, TradeDecisionArg, TradeDecisionArgs, TradeRequestArgs,
};
use crate::ops::{
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
OperationResult, OperationResultData, OperationService, TradeAcceptRequest, TradeAcceptResult,
- TradeAppExportRequest, TradeAppExportResult, TradeAppListRequest, TradeAppListResult,
TradeCancelRequest, TradeCancelResult, TradeDeclineRequest, TradeDeclineResult,
- TradeEventListRequest, TradeEventListResult, TradeEventWatchRequest, TradeEventWatchResult,
- TradeGetRequest, TradeGetResult, TradeListRequest, TradeListResult, TradeRebindRequest,
- TradeRebindResult, TradeStatusGetRequest, TradeStatusGetResult, TradeSubmitRequest,
- TradeSubmitResult,
+ TradeGetRequest, TradeGetResult, TradeListRequest, TradeListResult, TradeRequestRequest,
+ TradeRequestResult,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::view::runtime::{
- CommandDisposition, OrderAppRecordExportView, OrderCancellationView, OrderDecisionView,
- OrderRebindView, OrderStatusView, OrderSubmitView,
+ CommandDisposition, OrderCancellationView, OrderDecisionView, OrderSubmitView,
};
-const TRADE_EVENT_WATCH_DEFERRED_REASON: &str = "Nostr-backed trade event watch is not implemented";
-
pub struct TradeOperationService<'a> {
config: &'a RuntimeConfig,
}
@@ -36,21 +27,15 @@ impl<'a> TradeOperationService<'a> {
}
}
-impl OperationService<TradeSubmitRequest> for TradeOperationService<'_> {
- type Result = TradeSubmitResult;
+impl OperationService<TradeRequestRequest> for TradeOperationService<'_> {
+ type Result = TradeRequestResult;
fn execute(
&self,
- request: OperationRequest<TradeSubmitRequest>,
+ request: OperationRequest<TradeRequestRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
-
let key = required_trade_key(&request)?;
- let args = TradeSubmitArgs {
+ let args = TradeRequestArgs {
key,
idempotency_key: request
.context
@@ -66,7 +51,7 @@ impl OperationService<TradeSubmitRequest> for TradeOperationService<'_> {
let view = crate::runtime::order::submit(&config, &args).map_err(|error| {
OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
})?;
- submit_result::<TradeSubmitResult>(request.operation_id(), &view)
+ submit_result::<TradeRequestResult>(request.operation_id(), &view)
}
}
@@ -97,71 +82,6 @@ impl OperationService<TradeListRequest> for TradeOperationService<'_> {
}
}
-impl OperationService<TradeAppListRequest> for TradeOperationService<'_> {
- type Result = TradeAppListResult;
-
- fn execute(
- &self,
- _request: OperationRequest<TradeAppListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_runtime(crate::runtime::order::app_record_list(self.config))?;
- serialized_target_result::<TradeAppListResult, _>(&view)
- }
-}
-
-impl OperationService<TradeAppExportRequest> for TradeOperationService<'_> {
- type Result = TradeAppExportResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeAppExportRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradeAppRecordExportArgs {
- record_id: required_string_input(&request, "record_id")?,
- output: optional_path_input(&request, "output"),
- };
- let mut config = self.config.clone();
- if request.context.dry_run {
- config.output.dry_run = true;
- }
- let view = crate::runtime::order::app_record_export(&config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- order_app_record_export_result::<TradeAppExportResult>(request.operation_id(), &view)
- }
-}
-
-impl OperationService<TradeRebindRequest> for TradeOperationService<'_> {
- type Result = TradeRebindResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeRebindRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradeRebindArgs {
- key: required_trade_key(&request)?,
- selector: required_string_input(&request, "selector")?,
- };
- if request.context.dry_run {
- let view =
- crate::runtime::order::rebind_preflight(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- return order_rebind_result::<TradeRebindResult>(request.operation_id(), &view);
- }
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
-
- let view = crate::runtime::order::rebind(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- order_rebind_result::<TradeRebindResult>(request.operation_id(), &view)
- }
-}
-
impl OperationService<TradeAcceptRequest> for TradeOperationService<'_> {
type Result = TradeAcceptResult;
@@ -169,12 +89,6 @@ impl OperationService<TradeAcceptRequest> for TradeOperationService<'_> {
&self,
request: OperationRequest<TradeAcceptRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
-
let args = TradeDecisionArgs {
key: required_trade_key(&request)?,
decision: TradeDecisionArg::Accept,
@@ -213,11 +127,6 @@ impl OperationService<TradeDeclineRequest> for TradeOperationService<'_> {
"missing required `reason` input".to_owned(),
)
})?;
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
let args = TradeDecisionArgs {
key: required_trade_key(&request)?,
@@ -257,11 +166,6 @@ impl OperationService<TradeCancelRequest> for TradeOperationService<'_> {
"missing required `reason` input".to_owned(),
)
})?;
- if request.context.requires_approval_token() {
- return Err(OperationAdapterError::approval_required(
- request.operation_id(),
- ));
- }
let args = TradeCancelArgs {
key: required_trade_key(&request)?,
@@ -284,60 +188,6 @@ impl OperationService<TradeCancelRequest> for TradeOperationService<'_> {
}
}
-impl OperationService<TradeStatusGetRequest> for TradeOperationService<'_> {
- type Result = TradeStatusGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeStatusGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradeStatusArgs {
- key: required_trade_key(&request)?,
- };
- let view = crate::runtime::order::status(self.config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- status_result::<TradeStatusGetResult>(request.operation_id(), &view)
- }
-}
-
-impl OperationService<TradeEventListRequest> for TradeOperationService<'_> {
- type Result = TradeEventListResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeEventListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let trade_id = string_input(&request, "trade_id");
- let view = crate::runtime::order::event_list(self.config, trade_id.as_deref()).map_err(
- |error| OperationAdapterError::runtime_failure(request.operation_id(), error),
- )?;
- event_list_result::<TradeEventListResult>(request.operation_id(), &view)
- }
-}
-
-impl OperationService<TradeEventWatchRequest> for TradeOperationService<'_> {
- type Result = TradeEventWatchResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeEventWatchRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let trade_id = required_trade_key(&request)?;
- let action = format!("radroots trade status get {trade_id}");
- Err(OperationAdapterError::not_implemented_with_detail(
- request.operation_id(),
- TRADE_EVENT_WATCH_DEFERRED_REASON.to_owned(),
- json!({
- "state": "not_implemented",
- "trade_id": trade_id,
- "reason": TRADE_EVENT_WATCH_DEFERRED_REASON,
- "actions": [action],
- }),
- ))
- }
-}
-
fn decision_result<R>(
operation_id: &str,
view: &OrderDecisionView,
@@ -524,81 +374,6 @@ fn order_cancellation_error_detail(view: &OrderCancellationView) -> Value {
})
}
-fn status_result<R>(
- operation_id: &str,
- view: &OrderStatusView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_target_result::<R, _>(view),
- disposition => {
- let message = view
- .reason
- .clone()
- .unwrap_or_else(|| format!("order status finished with state `{}`", view.state));
- if disposition == CommandDisposition::ExternalUnavailable {
- let detail = order_status_error_detail(view);
- if !view.failed_transport_targets.is_empty()
- && view.attempted_transport_endpoints.is_empty()
- {
- Err(OperationAdapterError::network_unavailable_with_detail(
- operation_id,
- message,
- detail,
- ))
- } else {
- Err(OperationAdapterError::operation_unavailable_with_detail(
- operation_id,
- message,
- detail,
- ))
- }
- } else if disposition == CommandDisposition::Unconfigured {
- Err(OperationAdapterError::operation_unavailable_with_detail(
- operation_id,
- message,
- order_status_error_detail(view),
- ))
- } else {
- Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- message,
- ))
- }
- }
- }
-}
-
-fn order_status_error_detail(view: &OrderStatusView) -> Value {
- json!({
- "state": &view.state,
- "trade_id": &view.order_id,
- "locator": &view.locator,
- "request_event_id": &view.request_event_id,
- "decision_event_id": &view.decision_event_id,
- "agreement_event_id": &view.agreement_event_id,
- "listing_event_id": &view.listing_event_id,
- "listing_addr": &view.listing_addr,
- "buyer_pubkey": &view.buyer_pubkey,
- "seller_pubkey": &view.seller_pubkey,
- "last_event_id": &view.last_event_id,
- "inventory": &view.inventory,
- "lifecycle": &view.lifecycle,
- "sdk_receipt": &view.sdk_receipt,
- "reducer_issues": &view.reducer_issues,
- "target_transport_endpoints": &view.target_transport_endpoints,
- "attempted_transport_endpoints": &view.attempted_transport_endpoints,
- "failed_transport_targets": &view.failed_transport_targets,
- "fetched_count": view.fetched_count,
- "decoded_count": view.decoded_count,
- "skipped_count": view.skipped_count,
- "actions": &view.actions,
- })
-}
-
fn serialized_target_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
where
R: OperationResultData,
@@ -669,108 +444,6 @@ where
}
}
-fn order_app_record_export_result<R>(
- operation_id: &str,
- view: &OrderAppRecordExportView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_target_result::<R, _>(view),
- CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "app-authored local order record `{}` was not found",
- view.record_id
- )
- }),
- serde_json::to_value(view).unwrap_or(Value::Null),
- )),
- CommandDisposition::ValidationFailed => {
- Err(OperationAdapterError::validation_failed_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "app-authored local order record `{}` cannot be exported",
- view.record_id
- )
- }),
- serde_json::to_value(view).unwrap_or(Value::Null),
- ))
- }
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "app-authored local order record export finished with state `{}`",
- view.state
- )
- }),
- )),
- }
-}
-
-fn order_rebind_result<R>(
- operation_id: &str,
- view: &OrderRebindView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_target_result::<R, _>(view),
- CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
- operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| format!("order draft `{}` was not found", view.lookup)),
- order_rebind_error_detail(view),
- )),
- CommandDisposition::ValidationFailed => {
- Err(OperationAdapterError::validation_failed_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!("order rebind finished with state `{}`", view.state)
- }),
- order_rebind_error_detail(view),
- ))
- }
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason
- .clone()
- .unwrap_or_else(|| format!("order rebind finished with state `{}`", view.state)),
- )),
- }
-}
-
-fn order_rebind_error_detail(view: &OrderRebindView) -> Value {
- json!({
- "state": &view.state,
- "source": &view.source,
- "lookup": &view.lookup,
- "file": &view.file,
- "dry_run": view.dry_run,
- "from_order_id": &view.from_order_id,
- "to_order_id": &view.to_order_id,
- "order_id_changed": view.order_id_changed,
- "from_buyer_account_id": &view.from_buyer_account_id,
- "from_buyer_pubkey": &view.from_buyer_pubkey,
- "from_buyer_actor_source": &view.from_buyer_actor_source,
- "to_buyer_account_id": &view.to_buyer_account_id,
- "to_buyer_pubkey": &view.to_buyer_pubkey,
- "to_buyer_actor_source": &view.to_buyer_actor_source,
- "buyer_pubkey_changed": view.buyer_pubkey_changed,
- "existing_request_check": &view.existing_request_check,
- "existing_request_event_ids": &view.existing_request_event_ids,
- "actions": &view.actions,
- })
-}
-
fn order_submit_error_detail(view: &OrderSubmitView) -> Value {
json!({
"state": &view.state,
@@ -800,58 +473,6 @@ fn order_submit_error_detail(view: &OrderSubmitView) -> Value {
})
}
-fn event_list_result<R>(
- operation_id: &str,
- view: &crate::view::runtime::OrderEventListView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_target_result::<R, _>(view),
- disposition => {
- let message = view.reason.clone().unwrap_or_else(|| {
- format!("order event list finished with state `{}`", view.state)
- });
- if disposition == CommandDisposition::ExternalUnavailable {
- let detail = order_event_list_error_detail(view);
- Err(OperationAdapterError::network_unavailable_with_detail(
- operation_id,
- message,
- detail,
- ))
- } else if disposition == CommandDisposition::Unconfigured {
- Err(OperationAdapterError::operation_unavailable_with_detail(
- operation_id,
- message,
- order_event_list_error_detail(view),
- ))
- } else {
- Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- message,
- ))
- }
- }
- }
-}
-
-fn order_event_list_error_detail(view: &crate::view::runtime::OrderEventListView) -> Value {
- json!({
- "state": &view.state,
- "seller_pubkey": &view.seller_pubkey,
- "target_transport_endpoints": &view.target_transport_endpoints,
- "attempted_transport_endpoints": &view.attempted_transport_endpoints,
- "failed_transport_targets": &view.failed_transport_targets,
- "fetched_count": view.fetched_count,
- "decoded_count": view.decoded_count,
- "skipped_count": view.skipped_count,
- "count": view.count,
- "actions": &view.actions,
- })
-}
-
fn required_trade_key<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError>
where
P: OperationRequestPayload + OperationRequestData,
@@ -864,24 +485,6 @@ where
})
}
-fn required_string_input<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key)
- .map(|value| value.trim().to_owned())
- .filter(|value| !value.is_empty())
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
where
P: OperationRequestPayload + OperationRequestData,
@@ -894,13 +497,6 @@ where
.map(str::to_owned)
}
-fn optional_path_input<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).map(PathBuf::from)
-}
-
fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
where
P: OperationRequestPayload + OperationRequestData,
@@ -918,561 +514,3 @@ fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
message,
}
}
-
-#[cfg(test)]
-mod tests {
- use radroots_secret_vault::RadrootsSecretBackend;
- use serde_json::{Map, Value};
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- use super::{TradeOperationService, decision_result};
- use crate::ops::{
- OperationAdapter, OperationContext, OperationData, OperationRequest, TradeAcceptRequest,
- TradeAcceptResult, TradeCancelRequest, TradeDeclineRequest, TradeDeclineResult,
- TradeEventListRequest, TradeEventWatchRequest, TradeGetRequest, TradeListRequest,
- TradeStatusGetRequest, TradeSubmitRequest,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
- use crate::view::runtime::OrderDecisionView;
-
- #[test]
- fn order_service_get_and_list_preserve_order_truth() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let get = OperationRequest::new(
- OperationContext::default(),
- TradeGetRequest::from_data(data(&[("trade_id", "ord_missing")])),
- )
- .expect("order get request");
- let get_envelope = service
- .execute(get)
- .expect("order get result")
- .to_envelope(OperationContext::default().envelope_context("req_order_get"))
- .expect("order get envelope");
-
- assert_eq!(get_envelope.operation_id, "trade.get");
- assert_eq!(get_envelope.result["state"], "missing");
- assert_eq!(get_envelope.result["actions"][0], "radroots trade list");
- assert_eq!(get_envelope.result["actions"][1], "radroots basket create");
-
- let list = OperationRequest::new(OperationContext::default(), TradeListRequest::default())
- .expect("order list request");
- let list_envelope = service
- .execute(list)
- .expect("order list result")
- .to_envelope(OperationContext::default().envelope_context("req_order_list"))
- .expect("order list envelope");
- assert_eq!(list_envelope.operation_id, "trade.list");
- assert_eq!(list_envelope.result["state"], "empty");
- assert_eq!(list_envelope.result["actions"][0], "radroots basket create");
- }
-
- #[test]
- fn order_submit_requires_approval_token() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let submit = OperationRequest::new(
- OperationContext::default(),
- TradeSubmitRequest::from_data(data(&[("trade_id", "ord_missing")])),
- )
- .expect("order submit request");
- let error = service.execute(submit).expect_err("approval required");
-
- assert!(format!("{error}").contains("approval_token"));
- assert_eq!(error.to_output_error().code, "approval_required");
- assert_eq!(error.to_output_error().exit_code, 6);
- }
-
- #[test]
- fn order_submit_with_approval_returns_not_found_for_missing_order() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let mut context = OperationContext::default();
- context.approval_token = Some("approve_test".to_owned());
- let submit = OperationRequest::new(
- context.clone(),
- TradeSubmitRequest::from_data(data(&[("trade_id", "ord_missing")])),
- )
- .expect("order submit request");
- let error = service.execute(submit).expect_err("missing order error");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "not_found");
- assert_eq!(output_error.exit_code, 4);
- assert!(output_error.message.contains("ord_missing"));
- let envelope = crate::out::envelope::OutputEnvelope::failure(
- "trade.submit",
- output_error,
- context.envelope_context("req_order_submit"),
- );
- let detail = envelope.errors[0].detail.as_ref().expect("submit detail");
- assert_eq!(detail["state"], "missing");
- assert_eq!(detail["trade_id"], "ord_missing");
- assert_eq!(detail["actions"][0], "radroots trade list");
- assert_eq!(detail["actions"][1], "radroots basket create");
- assert_eq!(
- envelope.next_actions[0].command.as_deref(),
- Some("radroots trade list")
- );
- assert_eq!(
- envelope.next_actions[1].command.as_deref(),
- Some("radroots basket create")
- );
- }
-
- #[test]
- fn order_accept_requires_approval_token() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let accept = OperationRequest::new(
- OperationContext::default(),
- TradeAcceptRequest::from_data(data(&[("trade_id", "ord_pending")])),
- )
- .expect("order accept request");
- let error = service.execute(accept).expect_err("approval required");
-
- assert_eq!(error.to_output_error().code, "approval_required");
- }
-
- #[test]
- fn order_accept_unconfigured_preserves_decision_detail() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let mut context = OperationContext::default();
- context.dry_run = true;
- let accept = OperationRequest::new(
- context,
- TradeAcceptRequest::from_data(data(&[("trade_id", "ord_pending")])),
- )
- .expect("order accept request");
- let error = service
- .execute(accept)
- .expect_err("order accept unconfigured");
- let output_error = error.to_output_error();
- let detail = output_error.detail.as_ref().expect("decision detail");
-
- assert_eq!(output_error.code, "operation_unavailable");
- assert_eq!(detail["state"], "unconfigured");
- assert_eq!(detail["trade_id"], "ord_pending");
- assert_eq!(detail["decision"], "accepted");
- assert!(
- detail["target_transport_endpoints"]
- .as_array()
- .unwrap()
- .is_empty()
- );
- }
-
- #[test]
- fn order_decision_already_decided_maps_to_validation_failure() {
- let view = already_decided_view();
- let error = match decision_result::<TradeAcceptResult>("trade.accept", &view) {
- Ok(_) => panic!("already decided view should fail validation"),
- Err(error) => error,
- };
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "validation_failed");
- assert_eq!(output_error.exit_code, 10);
- let detail = output_error.detail.expect("validation detail");
- assert_eq!(detail["state"], "already_decided");
- assert_eq!(detail["operation_id"], "trade.accept");
- assert_eq!(detail["listing_event_id"], "l".repeat(64));
- assert_eq!(detail["event_id"], "d".repeat(64));
- assert_eq!(detail["event_kind"], 3423);
- assert_eq!(detail["idempotency_key"], "idem_test");
- assert_eq!(detail["signer_mode"], "local");
- assert_eq!(detail["actions"][0], "radroots trade status get ord_test");
- }
-
- #[test]
- fn order_decision_invalid_maps_to_validation_failure() {
- let view = invalid_decision_view();
- let error = match decision_result::<TradeDeclineResult>("trade.decline", &view) {
- Ok(_) => panic!("invalid view should fail validation"),
- Err(error) => error,
- };
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "validation_failed");
- assert_eq!(output_error.exit_code, 10);
- assert_eq!(
- output_error.message,
- "active trade events for `ord_test` failed reducer validation"
- );
- let detail = output_error.detail.expect("validation detail");
- assert_eq!(detail["state"], "invalid");
- assert_eq!(detail["operation_id"], "trade.decline");
- assert_eq!(detail["listing_event_id"], "l".repeat(64));
- assert_eq!(detail["event_id"], Value::Null);
- assert_eq!(detail["event_kind"], Value::Null);
- assert_eq!(detail["idempotency_key"], "idem_test");
- assert_eq!(detail["signer_mode"], "local");
- }
-
- #[test]
- fn order_decline_requires_reason_before_approval() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let decline = OperationRequest::new(
- OperationContext::default(),
- TradeDeclineRequest::from_data(data(&[("trade_id", "ord_pending")])),
- )
- .expect("order decline request");
- let error = service.execute(decline).expect_err("reason required");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "invalid_input");
- assert!(output_error.message.contains("reason"));
- }
-
- #[test]
- fn order_decline_rejects_blank_reason_before_approval() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let decline = OperationRequest::new(
- OperationContext::default(),
- TradeDeclineRequest::from_data(data(&[("trade_id", "ord_pending"), ("reason", " ")])),
- )
- .expect("order decline request");
- let error = service.execute(decline).expect_err("reason required");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "invalid_input");
- assert!(output_error.message.contains("reason"));
- }
-
- #[test]
- fn order_cancel_requires_reason_before_approval() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let cancel = OperationRequest::new(
- OperationContext::default(),
- TradeCancelRequest::from_data(data(&[("trade_id", "ord_pending")])),
- )
- .expect("order cancel request");
- let error = service.execute(cancel).expect_err("reason required");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "invalid_input");
- assert!(output_error.message.contains("reason"));
- }
-
- #[test]
- fn order_cancel_requires_approval_token() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let cancel = OperationRequest::new(
- OperationContext::default(),
- TradeCancelRequest::from_data(data(&[
- ("trade_id", "ord_pending"),
- ("reason", "changed plans"),
- ])),
- )
- .expect("order cancel request");
- let error = service.execute(cancel).expect_err("approval required");
-
- assert_eq!(error.to_output_error().code, "approval_required");
- }
-
- #[test]
- fn order_status_get_uses_local_sdk_projection_without_relay() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let status = OperationRequest::new(
- OperationContext::default(),
- TradeStatusGetRequest::from_data(data(&[("trade_id", "ord_pending")])),
- )
- .expect("order status request");
- let envelope = service
- .execute(status)
- .expect("status result")
- .to_envelope(OperationContext::default().envelope_context("req_order_status"))
- .expect("status envelope");
-
- assert_eq!(envelope.operation_id, "trade.status.get");
- assert_eq!(envelope.result["state"], "missing");
- assert_eq!(envelope.result["source"], "SDK local trade projection");
- assert_eq!(
- envelope.result["actor_context_source"],
- "sdk_local_projection"
- );
- assert_eq!(envelope.result["trade_id"], "ord_pending");
- assert_eq!(envelope.result["fetched_count"], 0);
- assert_eq!(envelope.result["decoded_count"], 0);
- assert_eq!(envelope.result["skipped_count"], 0);
- assert!(envelope.next_actions.is_empty());
- }
-
- #[test]
- fn order_event_list_requires_relay_configuration() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let context = OperationContext::default();
- let request = OperationRequest::new(context.clone(), TradeEventListRequest::default())
- .expect("order event list request");
- let output_error = service
- .execute(request)
- .expect_err("order event list unconfigured")
- .to_output_error();
- let envelope = crate::out::envelope::OutputEnvelope::failure(
- "trade.event.list",
- output_error,
- context.envelope_context("req_order_event_list"),
- );
-
- assert_eq!(envelope.errors[0].code, "operation_unavailable");
- assert_eq!(envelope.errors[0].exit_code, 3);
- assert!(
- envelope.errors[0]
- .message
- .contains("configured Nostr transport profile")
- );
- assert_eq!(
- envelope.errors[0].detail.as_ref().unwrap()["actions"][0],
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
- );
- assert_eq!(
- envelope.next_actions[0].command.as_deref(),
- Some(
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
- )
- );
- }
-
- #[test]
- fn order_event_list_requires_seller_account_with_account_action() {
- let dir = tempdir().expect("tempdir");
- let mut config = sample_config(dir.path());
- config.transport.nostr_relay_urls = vec!["ws://127.0.0.1:9".to_owned()];
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let context = OperationContext::default();
- let request = OperationRequest::new(context.clone(), TradeEventListRequest::default())
- .expect("order event list request");
- let output_error = service
- .execute(request)
- .expect_err("order event list missing account")
- .to_output_error();
- let envelope = crate::out::envelope::OutputEnvelope::failure(
- "trade.event.list",
- output_error,
- context.envelope_context("req_order_event_list"),
- );
-
- assert_eq!(envelope.errors[0].code, "operation_unavailable");
- assert!(
- envelope.errors[0]
- .message
- .contains("selected seller account")
- );
- assert_eq!(
- envelope.errors[0].detail.as_ref().unwrap()["actions"][0],
- "radroots account create"
- );
- assert_eq!(
- envelope.next_actions[0].command.as_deref(),
- Some("radroots account create")
- );
- }
-
- #[test]
- fn order_event_watch_returns_deferred_error_with_target_action() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(TradeOperationService::new(&config));
- let request = OperationRequest::new(
- OperationContext::default(),
- TradeEventWatchRequest::from_data(data(&[("trade_id", "ord_missing")])),
- )
- .expect("order event watch request");
- let error = service
- .execute(request)
- .expect_err("order event watch deferred");
- let envelope = crate::out::envelope::OutputEnvelope::failure(
- "trade.event.watch",
- error.to_output_error(),
- OperationContext::default().envelope_context("req_order_watch"),
- );
-
- assert_eq!(envelope.operation_id, "trade.event.watch");
- assert!(envelope.result.is_null());
- assert_eq!(envelope.errors[0].code, "not_implemented");
- assert_eq!(
- envelope.errors[0].detail.as_ref().unwrap()["state"],
- "not_implemented"
- );
- assert_eq!(
- envelope.errors[0].detail.as_ref().unwrap()["trade_id"],
- "ord_missing"
- );
- assert_eq!(
- envelope.next_actions[0].command.as_deref(),
- Some("radroots trade status get ord_missing")
- );
- }
-
- fn sample_config(root: &Path) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-
- fn data(entries: &[(&str, &str)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
- .collect::<Map<String, Value>>()
- }
-
- fn already_decided_view() -> OrderDecisionView {
- OrderDecisionView {
- state: "already_decided".to_owned(),
- source: "test".to_owned(),
- order_id: "ord_test".to_owned(),
- locator: crate::view::runtime::OrderTradeLocatorView {
- trade_id: "ord_test".to_owned(),
- root_event_id: Some("r".repeat(64)),
- listing_addr: Some("30402:seller:listing".to_owned()),
- buyer_pubkey: Some("b".repeat(64)),
- seller_pubkey: Some("s".repeat(64)),
- },
- listing_addr: Some("30402:seller:listing".to_owned()),
- buyer_pubkey: Some("b".repeat(64)),
- seller_pubkey: Some("s".repeat(64)),
- decision: "accepted".to_owned(),
- request_event_id: Some("r".repeat(64)),
- listing_event_id: Some("l".repeat(64)),
- root_event_id: Some("r".repeat(64)),
- prev_event_id: Some("r".repeat(64)),
- event_id: Some("d".repeat(64)),
- event_kind: Some(3423),
- inventory: None,
- dry_run: false,
- target_transport_endpoints: vec!["ws://relay.test".to_owned()],
- attempted_transport_endpoints: vec!["ws://relay.test".to_owned()],
- accepted_transport_endpoints: Vec::new(),
- failed_transport_targets: Vec::new(),
- fetched_count: 2,
- decoded_count: 2,
- skipped_count: 0,
- idempotency_key: Some("idem_test".to_owned()),
- signer_mode: Some("local".to_owned()),
- reason: Some(
- "order accept refused because order `ord_test` already has a visible `accepted` seller decision"
- .to_owned(),
- ),
- issues: Vec::new(),
- actions: vec!["radroots trade status get ord_test".to_owned()],
- }
- }
-
- fn invalid_decision_view() -> OrderDecisionView {
- let mut view = already_decided_view();
- view.state = "invalid".to_owned();
- view.decision = "declined".to_owned();
- view.event_id = None;
- view.event_kind = None;
- view.reason =
- Some("active trade events for `ord_test` failed reducer validation".to_owned());
- view
- }
-}
diff --git a/src/ops/exec/validation.rs b/src/ops/exec/validation.rs
@@ -4,13 +4,12 @@ use serde_json::{Value, json};
use crate::ops::{
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
OperationResult, OperationResultData, OperationService, ValidationReceiptGetRequest,
- ValidationReceiptGetResult, ValidationReceiptListRequest, ValidationReceiptListResult,
- ValidationReceiptVerifyRequest, ValidationReceiptVerifyResult,
+ ValidationReceiptGetResult, ValidationReceiptVerifyRequest, ValidationReceiptVerifyResult,
+ ValidationStatusRequest, ValidationStatusResult,
};
use crate::runtime::config::RuntimeConfig;
use crate::runtime::validation_receipt::{
- ValidationReceiptEventArgs, ValidationReceiptInspectionView, ValidationReceiptListArgs,
- ValidationReceiptListView,
+ ValidationReceiptEventArgs, ValidationReceiptInspectionView,
};
use crate::view::runtime::CommandDisposition;
@@ -24,6 +23,28 @@ impl<'a> ValidationOperationService<'a> {
}
}
+impl OperationService<ValidationStatusRequest> for ValidationOperationService<'_> {
+ type Result = ValidationStatusResult;
+
+ fn execute(
+ &self,
+ _request: OperationRequest<ValidationStatusRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let state = if self.config.rhi.validator_set.is_some() {
+ "ready"
+ } else {
+ "unconfigured"
+ };
+ let view = json!({
+ "state": state,
+ "source": "Runtime Contract V1 validation configuration",
+ "validator_set_configured": self.config.rhi.validator_set.is_some(),
+ "cryptographic_proof_required": self.config.rhi.require_cryptographic_proof,
+ });
+ serialized_operation_result::<ValidationStatusResult, _>(&view)
+ }
+}
+
impl OperationService<ValidationReceiptGetRequest> for ValidationOperationService<'_> {
type Result = ValidationReceiptGetResult;
@@ -40,19 +61,6 @@ impl OperationService<ValidationReceiptGetRequest> for ValidationOperationServic
}
}
-impl OperationService<ValidationReceiptListRequest> for ValidationOperationService<'_> {
- type Result = ValidationReceiptListResult;
-
- fn execute(
- &self,
- request: OperationRequest<ValidationReceiptListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = validation_receipt_list_args(&request)?;
- let view = crate::runtime::validation_receipt::list(self.config, &args);
- validation_receipt_list_result(&view)
- }
-}
-
impl OperationService<ValidationReceiptVerifyRequest> for ValidationOperationService<'_> {
type Result = ValidationReceiptVerifyResult;
@@ -80,17 +88,6 @@ where
})
}
-fn validation_receipt_list_args<P>(
- request: &OperationRequest<P>,
-) -> Result<ValidationReceiptListArgs, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- Ok(ValidationReceiptListArgs {
- order_id: required_string(request, "trade_id")?,
- })
-}
-
fn validation_receipt_inspection_result<R>(
operation_id: &str,
view: &ValidationReceiptInspectionView,
@@ -109,22 +106,6 @@ where
}
}
-fn validation_receipt_list_result(
- view: &ValidationReceiptListView,
-) -> Result<OperationResult<ValidationReceiptListResult>, OperationAdapterError> {
- match view.disposition() {
- CommandDisposition::Success => {
- serialized_operation_result::<ValidationReceiptListResult, _>(view)
- }
- disposition => Err(validation_receipt_view_error(
- "validation.receipt.list",
- disposition,
- view,
- view.reason.as_deref(),
- )),
- }
-}
-
fn validation_receipt_view_error<T>(
operation_id: &str,
disposition: CommandDisposition,
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
@@ -12,479 +12,3 @@ pub use error::OperationAdapterError;
pub use request::*;
pub use result::*;
pub use target::*;
-
-#[cfg(test)]
-mod tests {
- use std::io;
-
- use clap::Parser;
- use serde_json::{Value, json};
-
- use super::{
- OperationAdapter, OperationAdapterError, OperationContext, OperationInputMode,
- OperationNetworkMode, OperationOutputFormat, OperationRequest, OperationResult,
- OperationService, TargetOperationRequest, WorkspaceGetRequest, WorkspaceGetResult,
- adapter_registry_linkage_is_valid,
- };
- use crate::cli::TargetCliArgs;
- use crate::registry::OPERATION_REGISTRY;
- use crate::runtime::RuntimeError;
- use crate::runtime::account::AccountRuntimeFailure;
-
- #[test]
- fn adapter_binds_every_registry_entry() {
- assert!(adapter_registry_linkage_is_valid());
-
- for operation in OPERATION_REGISTRY {
- let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
- .unwrap_or_else(|error| {
- panic!("{} failed to parse: {error}", operation.cli_path);
- });
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
-
- assert_eq!(request.operation_id(), operation.operation_id);
- assert_eq!(request.spec().mcp_tool, operation.mcp_tool);
- assert_eq!(request.request_type_name(), operation.rust_request);
- assert_eq!(
- TargetOperationRequest::request_type_for_operation(operation.operation_id),
- Some(operation.rust_request)
- );
- }
- }
-
- #[test]
- fn adapter_context_carries_target_global_scope() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "--format",
- "json",
- "--account-id",
- "acct_test",
- "--online",
- "--dry-run",
- "--idempotency-key",
- "idem_test",
- "--correlation-id",
- "corr_test",
- "--approval-token",
- "approval_test",
- "--no-input",
- "--quiet",
- "--verbose",
- "--trace",
- "workspace",
- "get",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let context = request.context();
-
- assert_eq!(context.output_format, OperationOutputFormat::Json);
- assert_eq!(context.account_id.as_deref(), Some("acct_test"));
- assert_eq!(context.network_mode, OperationNetworkMode::Online);
- assert!(context.dry_run);
- assert_eq!(context.idempotency_key.as_deref(), Some("idem_test"));
- assert_eq!(context.correlation_id.as_deref(), Some("corr_test"));
- assert_eq!(context.approval_token.as_deref(), Some("approval_test"));
- assert_eq!(context.input_mode, OperationInputMode::NoInput);
- assert!(context.quiet);
- assert!(context.verbose);
- assert!(context.trace);
-
- let envelope_context = context.envelope_context("req_test");
- let actor = envelope_context.actor.expect("account actor");
- assert_eq!(actor.account_id, "acct_test");
- assert_eq!(actor.role, "account");
- }
-
- #[test]
- fn adapter_maps_account_attach_secret_input() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "account",
- "attach-secret",
- "acct_test",
- "identity.json",
- "--default",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::AccountAttachSecret(request) = request else {
- panic!("expected account attach-secret request")
- };
-
- assert_eq!(request.operation_id(), "account.attach_secret");
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- assert_eq!(
- request.payload.input.get("path").and_then(Value::as_str),
- Some("identity.json")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("default")
- .and_then(Value::as_bool),
- Some(true)
- );
- }
-
- #[test]
- fn adapter_maps_farm_rebind_selector() {
- let parsed = TargetCliArgs::try_parse_from(["radroots", "farm", "rebind", "acct_test"])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::FarmRebind(request) = request else {
- panic!("expected farm rebind request")
- };
-
- assert_eq!(request.operation_id(), "farm.rebind");
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- }
-
- #[test]
- fn adapter_maps_farm_location_set_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "farm",
- "location",
- "set",
- "--lat",
- "48.429456",
- "--lng",
- "-123.349786",
- "--label",
- "farm gate",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::FarmLocationSet(request) = request else {
- panic!("expected farm location set request")
- };
-
- assert_eq!(request.operation_id(), "farm.location.set");
- assert_eq!(
- request.payload.input.get("lat").and_then(Value::as_f64),
- Some(48.429456)
- );
- assert_eq!(
- request.payload.input.get("lng").and_then(Value::as_f64),
- Some(-123.349786)
- );
- assert_eq!(
- request.payload.input.get("label").and_then(Value::as_str),
- Some("farm gate")
- );
- assert!(!request.payload.input.contains_key("latitude"));
- assert!(!request.payload.input.contains_key("longitude"));
- }
-
- #[test]
- fn adapter_maps_listing_rebind_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "listing",
- "rebind",
- "listing.toml",
- "acct_test",
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::ListingRebind(request) = request else {
- panic!("expected listing rebind request")
- };
-
- assert_eq!(request.operation_id(), "listing.rebind");
- assert_eq!(
- request.payload.input.get("file").and_then(Value::as_str),
- Some("listing.toml")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("farm_d_tag")
- .and_then(Value::as_str),
- Some("AAAAAAAAAAAAAAAAAAAAAw")
- );
- }
-
- #[test]
- fn adapter_maps_order_rebind_inputs() {
- let parsed =
- TargetCliArgs::try_parse_from(["radroots", "trade", "rebind", "ord_test", "acct_test"])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::TradeRebind(request) = request else {
- panic!("expected order rebind request")
- };
-
- assert_eq!(request.operation_id(), "trade.rebind");
- assert_eq!(
- request
- .payload
- .input
- .get("trade_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- }
-
- #[test]
- fn adapter_maps_order_lifecycle_inputs() {
- let cancel = TargetCliArgs::try_parse_from([
- "radroots",
- "trade",
- "cancel",
- "ord_test",
- "--reason",
- "changed plans",
- "--confirm-public-note",
- ])
- .expect("target args parse");
- let request = TargetOperationRequest::from_target_args(&cancel).expect("operation request");
- let TargetOperationRequest::TradeCancel(request) = request else {
- panic!("expected order cancel request")
- };
- assert_eq!(request.operation_id(), "trade.cancel");
- assert_eq!(
- request
- .payload
- .input
- .get("trade_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request.payload.input.get("reason").and_then(Value::as_str),
- Some("changed plans")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("confirm_public_note")
- .and_then(Value::as_bool),
- Some(true)
- );
- }
-
- #[test]
- fn typed_service_boundary_returns_enveloped_result() {
- struct WorkspaceService;
-
- impl OperationService<WorkspaceGetRequest> for WorkspaceService {
- type Result = WorkspaceGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<WorkspaceGetRequest>,
- ) -> Result<OperationResult<Self::Result>, super::OperationAdapterError> {
- assert_eq!(request.operation_id(), "workspace.get");
- OperationResult::new(WorkspaceGetResult::default())
- }
- }
-
- let adapter = OperationAdapter::new(WorkspaceService);
- let context = OperationContext::default();
- let request = OperationRequest::new(context.clone(), WorkspaceGetRequest::default())
- .expect("typed request");
- let result = adapter.execute(request).expect("typed result");
- let envelope = result
- .to_envelope(context.envelope_context("req_test"))
- .expect("operation envelope");
-
- assert_eq!(envelope.operation_id, "workspace.get");
- assert_eq!(envelope.kind, "workspace.get");
- assert_eq!(envelope.request_id, "req_test");
- assert_eq!(envelope.result, json!({}));
- }
-
- #[test]
- fn approval_errors_map_to_structured_exit_code() {
- let error = OperationAdapterError::approval_required("trade.submit");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "approval_required");
- assert_eq!(output_error.exit_code, 6);
- assert!(output_error.message.contains("approval_token"));
- }
-
- #[test]
- fn not_implemented_errors_map_to_structured_exit_code() {
- let error =
- OperationAdapterError::not_implemented("test.operation", "coming soon".to_owned());
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "not_implemented");
- assert_eq!(output_error.exit_code, 3);
- assert_eq!(
- output_error.detail.expect("detail")["operation_id"],
- "test.operation"
- );
- }
-
- #[test]
- fn runtime_failures_map_to_specific_machine_codes() {
- let cases = [
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "no selected account for seller write".to_owned(),
- ),
- "account_unresolved",
- "account",
- 5,
- ),
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "resolved account `a` is watch_only and cannot sign because it is not secret-backed"
- .to_owned(),
- ),
- "account_watch_only",
- "account",
- 7,
- ),
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "account mismatch: resolved account pubkey `b` cannot sign listing seller_pubkey `a`"
- .to_owned(),
- ),
- "account_mismatch",
- "account",
- 5,
- ),
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "signer.remote_nip46 binding is missing".to_owned(),
- ),
- "signer_unconfigured",
- "signer",
- 7,
- ),
- (
- OperationAdapterError::unavailable(
- "listing.publish",
- "transport provider is unavailable".to_owned(),
- ),
- "provider_unavailable",
- "provider",
- 3,
- ),
- (
- OperationAdapterError::SignerModeDeferred {
- operation_id: "signer.status.get".to_owned(),
- message: "signer mode `myc` is deferred".to_owned(),
- },
- "signer_mode_deferred",
- "signer",
- 7,
- ),
- (
- OperationAdapterError::unconfigured(
- "basket.quote.create",
- "quote engine not ready".to_owned(),
- ),
- "operation_unavailable",
- "operation",
- 3,
- ),
- (
- OperationAdapterError::runtime_failure(
- "listing.publish",
- RuntimeError::Io(io::Error::new(io::ErrorKind::NotFound, "missing draft")),
- ),
- "not_found",
- "resource",
- 4,
- ),
- (
- OperationAdapterError::runtime_failure(
- "listing.validate",
- RuntimeError::Config("invalid listing draft listing.toml".to_owned()),
- ),
- "validation_failed",
- "validation",
- 10,
- ),
- (
- OperationAdapterError::runtime_failure(
- "listing.archive",
- RuntimeError::Account(AccountRuntimeFailure::mismatch(
- "account mismatch: resolved account pubkey `b` cannot sign listing seller_pubkey `a`",
- )),
- ),
- "account_mismatch",
- "account",
- 5,
- ),
- (
- OperationAdapterError::runtime_failure(
- "farm.publish",
- RuntimeError::Network("Nostr transport connection failed".to_owned()),
- ),
- "network_unavailable",
- "network",
- 8,
- ),
- ];
-
- for (error, code, class, exit_code) in cases {
- let output = error.to_output_error();
- assert_eq!(output.code, code);
- assert_eq!(output.exit_code, exit_code);
- assert_eq!(
- output.detail.expect("detail")["class"],
- serde_json::Value::String(class.to_owned())
- );
- }
- }
-}
diff --git a/src/ops/target.rs b/src/ops/target.rs
@@ -19,9 +19,10 @@ macro_rules! target_operation_contracts {
impl TargetOperationRequest {
pub fn from_target_args(args: &TargetCliArgs) -> Result<Self, OperationAdapterError> {
+ let operation_id = crate::cli::input::operation_id_from_target(args);
Self::from_operation_id_with_input(
- crate::cli::input::operation_id_from_target(args),
- OperationContext::from_target_args(args),
+ operation_id,
+ OperationContext::from_target_args(args, operation_id)?,
crate::cli::input::target_operation_input(&args.command),
)
}
@@ -173,89 +174,7 @@ macro_rules! target_operation_contracts {
};
}
-target_operation_contracts! {
- WorkspaceInit => (WorkspaceInitRequest, WorkspaceInitResult, "workspace.init"),
- WorkspaceGet => (WorkspaceGetRequest, WorkspaceGetResult, "workspace.get"),
- HealthStatusGet => (HealthStatusGetRequest, HealthStatusGetResult, "health.status.get"),
- HealthCheckRun => (HealthCheckRunRequest, HealthCheckRunResult, "health.check.run"),
- ConfigGet => (ConfigGetRequest, ConfigGetResult, "config.get"),
- AccountCreate => (AccountCreateRequest, AccountCreateResult, "account.create"),
- AccountImport => (AccountImportRequest, AccountImportResult, "account.import"),
- AccountAttachSecret => (AccountAttachSecretRequest, AccountAttachSecretResult, "account.attach_secret"),
- AccountGet => (AccountGetRequest, AccountGetResult, "account.get"),
- AccountList => (AccountListRequest, AccountListResult, "account.list"),
- AccountRemove => (AccountRemoveRequest, AccountRemoveResult, "account.remove"),
- AccountSelectionGet => (AccountSelectionGetRequest, AccountSelectionGetResult, "account.selection.get"),
- AccountSelectionUpdate => (AccountSelectionUpdateRequest, AccountSelectionUpdateResult, "account.selection.update"),
- AccountSelectionClear => (AccountSelectionClearRequest, AccountSelectionClearResult, "account.selection.clear"),
- SignerStatusGet => (SignerStatusGetRequest, SignerStatusGetResult, "signer.status.get"),
- TransportProfileGet => (TransportProfileGetRequest, TransportProfileGetResult, "transport.profile.get"),
- TransportProfileSet => (TransportProfileSetRequest, TransportProfileSetResult, "transport.profile.set"),
- TransportStatus => (TransportStatusRequest, TransportStatusResult, "transport.status"),
- TransportOutboxStatus => (TransportOutboxStatusRequest, TransportOutboxStatusResult, "transport.outbox.status"),
- TransportOutboxPush => (TransportOutboxPushRequest, TransportOutboxPushResult, "transport.outbox.push"),
- MeshScopeGet => (MeshScopeGetRequest, MeshScopeGetResult, "mesh.scope.get"),
- MeshScopeSet => (MeshScopeSetRequest, MeshScopeSetResult, "mesh.scope.set"),
- MeshStatus => (MeshStatusRequest, MeshStatusResult, "mesh.status"),
- MeshPolicyCheck => (MeshPolicyCheckRequest, MeshPolicyCheckResult, "mesh.policy.check"),
- StoreInit => (StoreInitRequest, StoreInitResult, "store.init"),
- StoreStatusGet => (StoreStatusGetRequest, StoreStatusGetResult, "store.status.get"),
- StoreExport => (StoreExportRequest, StoreExportResult, "store.export"),
- StoreBackupCreate => (StoreBackupCreateRequest, StoreBackupCreateResult, "store.backup.create"),
- StoreBackupRestore => (StoreBackupRestoreRequest, StoreBackupRestoreResult, "store.backup.restore"),
- SyncStatusGet => (SyncStatusGetRequest, SyncStatusGetResult, "sync.status.get"),
- SyncPull => (SyncPullRequest, SyncPullResult, "sync.pull"),
- SyncPush => (SyncPushRequest, SyncPushResult, "sync.push"),
- SyncWatch => (SyncWatchRequest, SyncWatchResult, "sync.watch"),
- FarmCreate => (FarmCreateRequest, FarmCreateResult, "farm.create"),
- FarmGet => (FarmGetRequest, FarmGetResult, "farm.get"),
- FarmRebind => (FarmRebindRequest, FarmRebindResult, "farm.rebind"),
- FarmProfileUpdate => (FarmProfileUpdateRequest, FarmProfileUpdateResult, "farm.profile.update"),
- FarmLocationSet => (FarmLocationSetRequest, FarmLocationSetResult, "farm.location.set"),
- FarmLocationGet => (FarmLocationGetRequest, FarmLocationGetResult, "farm.location.get"),
- FarmLocationClear => (FarmLocationClearRequest, FarmLocationClearResult, "farm.location.clear"),
- FarmFulfillmentUpdate => (FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult, "farm.fulfillment.update"),
- FarmReadinessCheck => (FarmReadinessCheckRequest, FarmReadinessCheckResult, "farm.readiness.check"),
- FarmPublish => (FarmPublishRequest, FarmPublishResult, "farm.publish"),
- ListingCreate => (ListingCreateRequest, ListingCreateResult, "listing.create"),
- ListingGet => (ListingGetRequest, ListingGetResult, "listing.get"),
- ListingList => (ListingListRequest, ListingListResult, "listing.list"),
- ListingAppList => (ListingAppListRequest, ListingAppListResult, "listing.app.list"),
- ListingAppExport => (ListingAppExportRequest, ListingAppExportResult, "listing.app.export"),
- ListingUpdate => (ListingUpdateRequest, ListingUpdateResult, "listing.update"),
- ListingValidate => (ListingValidateRequest, ListingValidateResult, "listing.validate"),
- ListingRebind => (ListingRebindRequest, ListingRebindResult, "listing.rebind"),
- ListingPublish => (ListingPublishRequest, ListingPublishResult, "listing.publish"),
- ListingArchive => (ListingArchiveRequest, ListingArchiveResult, "listing.archive"),
- MarketRefresh => (MarketRefreshRequest, MarketRefreshResult, "market.refresh"),
- MarketProductSearch => (MarketProductSearchRequest, MarketProductSearchResult, "market.product.search"),
- MarketListingGet => (MarketListingGetRequest, MarketListingGetResult, "market.listing.get"),
- BasketCreate => (BasketCreateRequest, BasketCreateResult, "basket.create"),
- BasketGet => (BasketGetRequest, BasketGetResult, "basket.get"),
- BasketList => (BasketListRequest, BasketListResult, "basket.list"),
- BasketItemAdd => (BasketItemAddRequest, BasketItemAddResult, "basket.item.add"),
- BasketItemUpdate => (BasketItemUpdateRequest, BasketItemUpdateResult, "basket.item.update"),
- BasketItemRemove => (BasketItemRemoveRequest, BasketItemRemoveResult, "basket.item.remove"),
- BasketAdjustmentAdd => (BasketAdjustmentAddRequest, BasketAdjustmentAddResult, "basket.adjustment.add"),
- BasketAdjustmentRemove => (BasketAdjustmentRemoveRequest, BasketAdjustmentRemoveResult, "basket.adjustment.remove"),
- BasketValidate => (BasketValidateRequest, BasketValidateResult, "basket.validate"),
- BasketQuoteCreate => (BasketQuoteCreateRequest, BasketQuoteCreateResult, "basket.quote.create"),
- TradeSubmit => (TradeSubmitRequest, TradeSubmitResult, "trade.submit"),
- TradeGet => (TradeGetRequest, TradeGetResult, "trade.get"),
- TradeList => (TradeListRequest, TradeListResult, "trade.list"),
- TradeAppList => (TradeAppListRequest, TradeAppListResult, "trade.app.list"),
- TradeAppExport => (TradeAppExportRequest, TradeAppExportResult, "trade.app.export"),
- TradeRebind => (TradeRebindRequest, TradeRebindResult, "trade.rebind"),
- TradeAccept => (TradeAcceptRequest, TradeAcceptResult, "trade.accept"),
- TradeDecline => (TradeDeclineRequest, TradeDeclineResult, "trade.decline"),
- TradeCancel => (TradeCancelRequest, TradeCancelResult, "trade.cancel"),
- TradeStatusGet => (TradeStatusGetRequest, TradeStatusGetResult, "trade.status.get"),
- TradeEventList => (TradeEventListRequest, TradeEventListResult, "trade.event.list"),
- TradeEventWatch => (TradeEventWatchRequest, TradeEventWatchResult, "trade.event.watch"),
- ValidationReceiptGet => (ValidationReceiptGetRequest, ValidationReceiptGetResult, "validation.receipt.get"),
- ValidationReceiptList => (ValidationReceiptListRequest, ValidationReceiptListResult, "validation.receipt.list"),
- ValidationReceiptVerify => (ValidationReceiptVerifyRequest, ValidationReceiptVerifyResult, "validation.receipt.verify"),
-}
+include!("../generated/runtime_contract_target.rs");
pub fn adapter_registry_linkage_is_valid() -> bool {
OPERATION_REGISTRY.iter().all(|operation| {
diff --git a/src/out/envelope.rs b/src/out/envelope.rs
@@ -326,7 +326,7 @@ fn next_action_label(command: &str) -> String {
| "--account-id"
| "--idempotency-key"
| "--correlation-id"
- | "--approval-token"
+ | "--approval-proof"
) && index < parts.len()
{
index += 1;
@@ -543,18 +543,18 @@ mod tests {
fn failure_envelope_carries_structured_error_and_exit_code() {
let error = OutputError::new(
"approval_required",
- "operation requires approval token",
+ "operation requires approval proof",
CliExitCode::ApprovalRequiredOrDenied,
);
let envelope = OutputEnvelope::failure(
- "trade.submit",
+ "trade.request",
error,
EnvelopeContext::new("req_order", false),
);
let value = serde_json::to_value(envelope).expect("serialize envelope");
assert_eq!(value["schema_version"], OUTPUT_SCHEMA_VERSION);
- assert_eq!(value["operation_id"], "trade.submit");
+ assert_eq!(value["operation_id"], "trade.request");
assert_eq!(value["status"], "error");
assert_eq!(value["reason_code"], "approval_required");
assert_eq!(value["result"], Value::Null);
@@ -577,7 +577,7 @@ mod tests {
]
}));
let envelope = OutputEnvelope::failure(
- "trade.submit",
+ "trade.request",
error,
EnvelopeContext::new("req_order", true),
);
@@ -612,7 +612,7 @@ mod tests {
]
}));
let envelope = OutputEnvelope::failure(
- "config.get",
+ "transport.config.inspect",
error,
EnvelopeContext::new("req_config", false),
);
@@ -655,21 +655,21 @@ mod tests {
fn ndjson_frames_serialize_one_json_object_per_line() {
let frames = [
NdjsonFrame::new(
- "sync.watch",
+ "sync.status",
"req_watch",
0,
NdjsonFrameType::Started,
json!({ "state": "started" }),
),
NdjsonFrame::new(
- "sync.watch",
+ "sync.status",
"req_watch",
1,
NdjsonFrameType::Event,
json!({ "state": "submitted" }),
),
NdjsonFrame::new(
- "sync.watch",
+ "sync.status",
"req_watch",
2,
NdjsonFrameType::Completed,
@@ -685,7 +685,7 @@ mod tests {
for line in rendered.lines() {
let value: Value = serde_json::from_str(line).expect("line is json");
assert_eq!(value["schema_version"], OUTPUT_SCHEMA_VERSION);
- assert_eq!(value["operation_id"], "sync.watch");
+ assert_eq!(value["operation_id"], "sync.status");
assert!(value["frame_type"].is_string());
}
}
diff --git a/src/out/terminal/actions.rs b/src/out/terminal/actions.rs
@@ -269,10 +269,10 @@ mod tests {
#[test]
fn exact_command_renders_as_command() {
- let action = terminal_action_from_command("radroots store init");
+ let action = terminal_action_from_command("radroots store inspect");
assert_eq!(action.kind, TerminalActionKind::Command);
- assert_eq!(action.command.as_deref(), Some("radroots store init"));
+ assert_eq!(action.command.as_deref(), Some("radroots store inspect"));
}
#[test]
diff --git a/src/out/terminal/errors.rs b/src/out/terminal/errors.rs
@@ -42,7 +42,7 @@ mod tests {
fn builds_terminal_error_document() {
let error = OutputError::new("invalid_input", "missing input", CliExitCode::InvalidInput);
let envelope = OutputEnvelope::failure(
- "trade.submit",
+ "trade.request",
error,
EnvelopeContext::new("req_test", false),
);
diff --git a/src/out/terminal/registry.rs b/src/out/terminal/registry.rs
@@ -65,6 +65,10 @@ impl TerminalRendererRegistry {
self.entries.len()
}
+ pub fn is_empty(&self) -> bool {
+ self.entries.is_empty()
+ }
+
pub fn operation_ids(&self) -> impl Iterator<Item = &'static str> + '_ {
self.entries.iter().map(|entry| entry.operation_id)
}
@@ -97,11 +101,11 @@ impl TerminalRendererRegistry {
*counts.entry(operation_id).or_default() += 1;
}
let mut violations = self.violations.clone();
- for operation_id in expected {
+ for operation_id in &expected {
match counts.get(operation_id).copied().unwrap_or(0) {
0 => {
violations.push(TerminalRendererRegistryViolation::missing(
- operation_id.to_owned(),
+ (*operation_id).to_owned(),
));
}
1 => {}
@@ -113,6 +117,11 @@ impl TerminalRendererRegistry {
}
}
}
+ for operation_id in counts.keys() {
+ if !expected.contains(operation_id) {
+ violations.push(TerminalRendererRegistryViolation::extra(*operation_id));
+ }
+ }
violations.sort();
violations.dedup();
if violations.is_empty() {
@@ -126,6 +135,7 @@ impl TerminalRendererRegistry {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum TerminalRendererRegistryViolation {
DuplicateOperation { operation_id: String, count: usize },
+ ExtraOperation { operation_id: String },
MissingOperation { operation_id: String },
}
@@ -144,6 +154,12 @@ impl TerminalRendererRegistryViolation {
}
}
+ fn extra(operation_id: impl Into<String>) -> Self {
+ Self::ExtraOperation {
+ operation_id: operation_id.into(),
+ }
+ }
+
fn missing(operation_id: impl Into<String>) -> Self {
Self::MissingOperation {
operation_id: operation_id.into(),
@@ -153,6 +169,7 @@ impl TerminalRendererRegistryViolation {
pub fn kind(&self) -> &'static str {
match self {
Self::DuplicateOperation { .. } => "duplicate",
+ Self::ExtraOperation { .. } => "extra",
Self::MissingOperation { .. } => "missing",
}
}
@@ -160,6 +177,7 @@ impl TerminalRendererRegistryViolation {
pub fn operation_id(&self) -> &str {
match self {
Self::DuplicateOperation { operation_id, .. }
+ | Self::ExtraOperation { operation_id }
| Self::MissingOperation { operation_id } => operation_id,
}
}
@@ -167,7 +185,7 @@ impl TerminalRendererRegistryViolation {
pub fn count(&self) -> Option<usize> {
match self {
Self::DuplicateOperation { count, .. } => Some(*count),
- Self::MissingOperation { .. } => None,
+ Self::ExtraOperation { .. } | Self::MissingOperation { .. } => None,
}
}
}
@@ -201,6 +219,9 @@ impl fmt::Display for TerminalRendererRegistryError {
formatter,
"; duplicate renderer for {operation_id} ({count})"
)?,
+ TerminalRendererRegistryViolation::ExtraOperation { operation_id } => {
+ write!(formatter, "; unexpected renderer for {operation_id}")?
+ }
TerminalRendererRegistryViolation::MissingOperation { operation_id } => {
write!(formatter, "; missing renderer for {operation_id}")?
}
@@ -212,19 +233,7 @@ impl fmt::Display for TerminalRendererRegistryError {
pub fn terminal_renderer_registry() -> TerminalRendererRegistry {
let registry = TerminalRendererRegistry::new();
- let registry = crate::out::terminal::renderers::workspace::register(registry);
- let registry = crate::out::terminal::renderers::health::register(registry);
- let registry = crate::out::terminal::renderers::config::register(registry);
- let registry = crate::out::terminal::renderers::account::register(registry);
- let registry = crate::out::terminal::renderers::runtime::register(registry);
- let registry = crate::out::terminal::renderers::store::register(registry);
- let registry = crate::out::terminal::renderers::sync::register(registry);
- let registry = crate::out::terminal::renderers::farm::register(registry);
- let registry = crate::out::terminal::renderers::listing::register(registry);
- let registry = crate::out::terminal::renderers::market::register(registry);
- let registry = crate::out::terminal::renderers::basket::register(registry);
- let registry = crate::out::terminal::renderers::trade::register(registry);
- crate::out::terminal::renderers::validation::register(registry)
+ crate::out::terminal::renderers::v1::register(registry)
}
#[cfg(test)]
@@ -250,42 +259,60 @@ mod tests {
#[test]
fn registers_operation_renderers() {
- let registry = TerminalRendererRegistry::new().register("workspace.get", &TEST_RENDERER);
+ let registry = TerminalRendererRegistry::new().register("profile.inspect", &TEST_RENDERER);
- assert!(registry.contains("workspace.get"));
+ assert!(registry.contains("profile.inspect"));
assert_eq!(registry.len(), 1);
- assert!(registry.get("workspace.get").is_some());
+ assert!(registry.get("profile.inspect").is_some());
assert!(registry.get("missing").is_none());
}
#[test]
fn duplicate_operation_renderer_registration_fails() {
let registry = TerminalRendererRegistry::new()
- .register("workspace.get", &TEST_RENDERER)
- .register("workspace.get", &TEST_RENDERER);
+ .register("profile.inspect", &TEST_RENDERER)
+ .register("profile.inspect", &TEST_RENDERER);
let error = registry
- .validate_operation_ids(["workspace.get"])
+ .validate_operation_ids(["profile.inspect"])
.expect_err("duplicate renderer registration should fail");
assert_eq!(
error.violations(),
&[TerminalRendererRegistryViolation::duplicate_count(
- "workspace.get",
+ "profile.inspect",
2
)]
);
- assert!(registry.get("workspace.get").is_none());
+ assert!(registry.get("profile.inspect").is_none());
}
#[test]
fn missing_operation_renderer_registration_fails() {
let error = TerminalRendererRegistry::new()
- .validate_operation_ids(["workspace.get"])
+ .validate_operation_ids(["profile.inspect"])
.expect_err("missing renderer registration should fail");
assert_eq!(
error.violations(),
- &[TerminalRendererRegistryViolation::missing("workspace.get")]
+ &[TerminalRendererRegistryViolation::missing(
+ "profile.inspect"
+ )]
+ );
+ }
+
+ #[test]
+ fn extra_operation_renderer_registration_fails() {
+ let error = TerminalRendererRegistry::new()
+ .register("test.extra", &TEST_RENDERER)
+ .validate_operation_ids(["profile.inspect"])
+ .expect_err("extra renderer registration should fail");
+
+ assert_eq!(
+ error.violations(),
+ &[
+ TerminalRendererRegistryViolation::extra("test.extra"),
+ TerminalRendererRegistryViolation::missing("profile.inspect"),
+ ]
);
}
diff --git a/src/out/terminal/renderer.rs b/src/out/terminal/renderer.rs
@@ -256,9 +256,7 @@ mod tests {
"Details",
vec!["extra context".to_owned()],
))
- .with_next(TerminalAction::command(
- "radroots trade status get ord_test",
- ));
+ .with_next(TerminalAction::command("radroots trade get ord_test"));
document.warnings = vec![TerminalWarning::new("warn_test", "suppressed")];
document.reference = Some(TerminalReference {
request_id: Some("req_test".to_owned()),
diff --git a/src/out/terminal/renderers/account.rs b/src/out/terminal/renderers/account.rs
@@ -1,125 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::{TerminalDocument, TerminalSection};
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("account.create", &ACCOUNT_RENDERER)
- .register("account.import", &ACCOUNT_RENDERER)
- .register("account.attach_secret", &ACCOUNT_RENDERER)
- .register("account.get", &ACCOUNT_RENDERER)
- .register("account.list", &ACCOUNT_RENDERER)
- .register("account.remove", &ACCOUNT_RENDERER)
- .register("account.selection.get", &ACCOUNT_RENDERER)
- .register("account.selection.update", &ACCOUNT_RENDERER)
- .register("account.selection.clear", &ACCOUNT_RENDERER)
-}
-
-struct AccountRenderer;
-
-static ACCOUNT_RENDERER: AccountRenderer = AccountRenderer;
-
-impl TerminalOperationRenderer for AccountRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::result(envelope);
- let mut document =
- common::document_with_title(envelope, account_title(envelope.operation_id.as_str()));
- match envelope.operation_id.as_str() {
- "account.list" => add_account_list(&mut document, result),
- _ => add_account_fields(&mut document, result),
- }
- document
- }
-}
-
-fn account_title(operation_id: &str) -> &'static str {
- match operation_id {
- "account.create" => "Account created",
- "account.import" => "Account imported",
- "account.attach_secret" => "Account secret attached",
- "account.get" => "Account",
- "account.list" => "Accounts",
- "account.remove" => "Account removed",
- "account.selection.get" => "Account selection",
- "account.selection.update" => "Account selection updated",
- "account.selection.clear" => "Account selection cleared",
- _ => "Account",
- }
-}
-
-fn add_account_fields(document: &mut TerminalDocument, result: &Value) {
- for path in [
- &["account", "id"][..],
- &["account_id"][..],
- &["resolved_account", "account_id"][..],
- &["default_account", "account_id"][..],
- &["selected_account", "account_id"][..],
- &["removed_account", "id"][..],
- ] {
- if let Some(account_id) = common::string(result, path) {
- common::push_field(document, "Account", account_id);
- break;
- }
- }
- common::push_path_field(document, "Label", result, &["account", "label"]);
- common::push_path_field(document, "Label", result, &["removed_account", "label"]);
- common::push_path_field(document, "Public key", result, &["account", "public_key"]);
- common::push_path_field(
- document,
- "Public key",
- result,
- &["removed_account", "public_key"],
- );
- common::push_bool_field(
- document,
- "Write capable",
- result,
- &["account", "write_capable"],
- );
- common::push_bool_field(
- document,
- "Write capable",
- result,
- &["removed_account", "write_capable"],
- );
- common::push_path_field(document, "Source", result, &["source"]);
-}
-
-fn add_account_list(document: &mut TerminalDocument, result: &Value) {
- common::push_count_field(document, "Count", result, &["count"]);
- let rows = common::array(result, &["accounts"])
- .into_iter()
- .flatten()
- .map(|account| {
- TerminalTableRow::new(vec![
- common::string(account, &["id"]).unwrap_or_default(),
- common::string(account, &["label"]).unwrap_or_default(),
- common::string(account, &["public_key"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Accounts",
- vec![
- TerminalTableColumn::new("Account", 7, 22),
- TerminalTableColumn::new("Label", 5, 18),
- TerminalTableColumn::new("Public key", 10, 24),
- ],
- rows,
- "No accounts found",
- ));
- if let Some(source) = common::string(result, &["source"]) {
- document
- .sections
- .push(TerminalSection::lines("Source", vec![source]));
- }
-}
diff --git a/src/out/terminal/renderers/basket.rs b/src/out/terminal/renderers/basket.rs
@@ -1,405 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("basket.create", &BASKET_RENDERER)
- .register("basket.get", &BASKET_RENDERER)
- .register("basket.list", &BASKET_RENDERER)
- .register("basket.item.add", &BASKET_RENDERER)
- .register("basket.item.update", &BASKET_RENDERER)
- .register("basket.item.remove", &BASKET_RENDERER)
- .register("basket.adjustment.add", &BASKET_RENDERER)
- .register("basket.adjustment.remove", &BASKET_RENDERER)
- .register("basket.validate", &BASKET_RENDERER)
- .register("basket.quote.create", &BASKET_RENDERER)
-}
-
-struct BasketRenderer;
-
-static BASKET_RENDERER: BasketRenderer = BasketRenderer;
-
-impl TerminalOperationRenderer for BasketRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- match envelope.operation_id.as_str() {
- "basket.list" => basket_list_document(envelope, result),
- "basket.validate" => basket_validate_document(envelope, result),
- "basket.quote.create" => basket_quote_document(envelope, result),
- "basket.item.add" | "basket.item.update" | "basket.item.remove" => {
- basket_mutation_document(envelope, result)
- }
- "basket.adjustment.add" | "basket.adjustment.remove" => {
- basket_mutation_document(envelope, result)
- }
- _ => basket_detail_document(envelope, result),
- }
- }
-}
-
-fn basket_detail_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, basket_title(envelope.operation_id.as_str()));
- push_basket_fields(&mut document, result);
- push_items_section(&mut document, result);
- push_adjustments_section(&mut document, result);
- push_issues_section(&mut document, result);
- document
-}
-
-fn basket_mutation_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, basket_title(envelope.operation_id.as_str()));
- push_basket_fields(&mut document, result);
- push_single_item_fields(&mut document, result);
- push_single_adjustment_fields(&mut document, result);
- push_items_section(&mut document, result);
- push_adjustments_section(&mut document, result);
- push_issues_section(&mut document, result);
- document
-}
-
-fn basket_validate_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Basket validation");
- push_basket_fields(&mut document, result);
- push_issues_section(&mut document, result);
- document
-}
-
-fn basket_quote_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Basket quote");
- push_basket_fields(&mut document, result);
- common::push_path_field(&mut document, "Quote", result, &["quote", "quote_id"]);
- common::push_path_field(&mut document, "Trade", result, &["quote", "trade_id"]);
- common::push_path_field(&mut document, "Trade", result, &["trade", "trade_id"]);
- common::push_bool_field(
- &mut document,
- "Submit ready",
- result,
- &["quote", "ready_for_submit"],
- );
- push_single_item_fields(&mut document, result);
- push_issues_section(&mut document, result);
- push_trade_issues_section(&mut document, result);
- document
-}
-
-fn basket_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Baskets");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- let rows = common::array(result, &["baskets"])
- .into_iter()
- .flatten()
- .map(|basket| {
- TerminalTableRow::new(vec![
- common::string(basket, &["basket_id"]).unwrap_or_default(),
- common::string(basket, &["state"]).unwrap_or_default(),
- common::number_label_path(basket, &["item_count"]).unwrap_or_default(),
- common::bool_path(basket, &["ready_for_quote"])
- .map(|ready| if ready { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Baskets",
- vec![
- TerminalTableColumn::new("Basket", 6, 24),
- TerminalTableColumn::new("State", 5, 14),
- TerminalTableColumn::new("Items", 5, 5),
- TerminalTableColumn::new("Quote", 5, 5),
- ],
- rows,
- "No baskets found",
- ));
- document
-}
-
-fn push_basket_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(document, "State", result, &["state"]);
- common::push_path_field(document, "Basket", result, &["basket_id"]);
- common::push_path_field(document, "Lookup", result, &["lookup"]);
- common::push_count_field(document, "Items", result, &["item_count"]);
- common::push_count_field(document, "Adjustments", result, &["adjustment_count"]);
- common::push_bool_field(document, "Quote ready", result, &["ready_for_quote"]);
- common::push_path_field(document, "File", result, &["file"]);
- common::push_path_field(document, "Reason", result, &["reason"]);
-}
-
-fn push_single_item_fields(document: &mut TerminalDocument, result: &Value) {
- let Some(item) = result.get("item") else {
- return;
- };
- common::push_path_field(document, "Item", item, &["item_id"]);
- common::push_path_field(document, "Listing", item, &["listing"]);
- common::push_path_field(document, "Listing", item, &["listing_addr"]);
- common::push_path_field(document, "Bin", item, &["bin_id"]);
- common::push_count_field(document, "Quantity", item, &["quantity"]);
-}
-
-fn push_single_adjustment_fields(document: &mut TerminalDocument, result: &Value) {
- let Some(adjustment) = result.get("adjustment") else {
- return;
- };
- common::push_path_field(document, "Adjustment", adjustment, &["id"]);
- common::push_path_field(document, "Effect", adjustment, &["effect"]);
- common::push_path_field(document, "Amount", adjustment, &["amount"]);
- common::push_path_field(document, "Currency", adjustment, &["currency"]);
- common::push_path_field(document, "Reason", adjustment, &["reason"]);
-}
-
-fn push_items_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["items"])
- .into_iter()
- .flatten()
- .map(|item| {
- TerminalTableRow::new(vec![
- common::string(item, &["item_id"]).unwrap_or_default(),
- common::string(item, &["listing"])
- .or_else(|| common::string(item, &["listing_addr"]))
- .unwrap_or_default(),
- common::string(item, &["bin_id"]).unwrap_or_default(),
- common::number_label_path(item, &["quantity"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Items",
- vec![
- TerminalTableColumn::new("Item", 4, 12),
- TerminalTableColumn::new("Listing", 8, 34),
- TerminalTableColumn::new("Bin", 3, 10),
- TerminalTableColumn::new("Qty", 3, 5),
- ],
- rows,
- "No basket items",
- ));
-}
-
-fn push_adjustments_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["adjustments"])
- .into_iter()
- .flatten()
- .map(|adjustment| {
- TerminalTableRow::new(vec![
- common::string(adjustment, &["id"]).unwrap_or_default(),
- common::string(adjustment, &["effect"]).unwrap_or_default(),
- common::string(adjustment, &["amount"]).unwrap_or_default(),
- common::string(adjustment, &["reason"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Adjustments",
- vec![
- TerminalTableColumn::new("ID", 2, 14),
- TerminalTableColumn::new("Effect", 6, 9),
- TerminalTableColumn::new("Amount", 6, 10),
- TerminalTableColumn::new("Reason", 6, 24),
- ],
- rows,
- "No basket adjustments",
- ));
-}
-
-fn push_issues_section(document: &mut TerminalDocument, result: &Value) {
- let rows = issue_rows(result, &["issues"]);
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Issues",
- vec![
- TerminalTableColumn::new("Code", 8, 32),
- TerminalTableColumn::new("Field", 8, 24),
- TerminalTableColumn::new("Message", 7, 48),
- ],
- rows,
- "No basket issues",
- ));
-}
-
-fn push_trade_issues_section(document: &mut TerminalDocument, result: &Value) {
- let rows = issue_rows(result, &["trade", "issues"]);
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Trade issues",
- vec![
- TerminalTableColumn::new("Code", 8, 32),
- TerminalTableColumn::new("Field", 8, 24),
- TerminalTableColumn::new("Message", 7, 48),
- ],
- rows,
- "No trade issues",
- ));
-}
-
-fn issue_rows(result: &Value, path: &[&str]) -> Vec<TerminalTableRow> {
- common::array(result, path)
- .into_iter()
- .flatten()
- .map(|issue| {
- TerminalTableRow::new(vec![
- common::string(issue, &["code"]).unwrap_or_default(),
- common::string(issue, &["field"]).unwrap_or_default(),
- common::string(issue, &["message"]).unwrap_or_default(),
- ])
- })
- .collect()
-}
-
-fn basket_title(operation_id: &str) -> &'static str {
- match operation_id {
- "basket.create" => "Basket created",
- "basket.get" => "Basket",
- "basket.item.add" => "Basket item added",
- "basket.item.update" => "Basket item updated",
- "basket.item.remove" => "Basket item removed",
- "basket.adjustment.add" => "Basket adjustment added",
- "basket.adjustment.remove" => "Basket adjustment removed",
- _ => "Basket",
- }
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::json;
-
- use crate::out::envelope::{EnvelopeContext, next_actions_from_result_value};
- use crate::out::terminal::renderer::render_terminal_document;
-
- use super::*;
-
- fn success_envelope(operation_id: &str, result: Value, dry_run: bool) -> OutputEnvelope {
- let mut envelope = OutputEnvelope::success(
- operation_id,
- result,
- EnvelopeContext::new(format!("req_{operation_id}"), dry_run),
- );
- envelope.next_actions = next_actions_from_result_value(&envelope.result);
- envelope
- }
-
- #[test]
- fn empty_list_renders_stable_empty_table() {
- let envelope = success_envelope(
- "basket.list",
- json!({
- "state": "empty",
- "count": 0,
- "baskets": [],
- "actions": ["radroots basket create"]
- }),
- false,
- );
- let document = BASKET_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Baskets"));
- assert!(rendered.contains("No baskets found"));
- assert!(rendered.contains("radroots basket create"));
- }
-
- #[test]
- fn validation_issues_render_as_issue_table() {
- let envelope = success_envelope(
- "basket.validate",
- json!({
- "state": "unconfigured",
- "basket_id": "basket_test",
- "ready_for_quote": false,
- "item_count": 1,
- "adjustment_count": 0,
- "issues": [{
- "code": "basket_market_replica_missing",
- "field": "local.replica_store",
- "message": "current local replica data is required before quote creation"
- }],
- "actions": ["radroots basket get basket_test"]
- }),
- false,
- );
- let document = BASKET_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Issues"));
- assert!(rendered.contains("basket_market_replica_missing"));
- assert!(rendered.contains("local.replica_store"));
- }
-
- #[test]
- fn incomplete_buyer_actions_are_placeholdered() {
- let envelope = success_envelope(
- "basket.item.add",
- json!({
- "state": "dry_run",
- "basket_id": "basket_test",
- "item": {
- "item_id": "item_1",
- "listing": "eggs",
- "bin_id": "bin-1",
- "quantity": 1
- },
- "actions": ["radroots basket item add"]
- }),
- true,
- );
- let document = BASKET_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(
- rendered
- .contains("radroots basket item add <basket> --listing <product> --bin-id <bin>")
- );
- }
-
- #[test]
- fn quote_next_action_uses_trade_surface() {
- let envelope = success_envelope(
- "basket.quote.create",
- json!({
- "state": "quoted",
- "basket_id": "basket_test",
- "quote": {
- "quote_id": "quote_test",
- "trade_id": "trade_test",
- "trade_file": "internal-draft-path",
- "ready_for_submit": true
- },
- "trade": {
- "trade_id": "trade_test",
- "file": "internal-draft-path",
- "ready_for_submit": true
- },
- "actions": ["radroots trade submit trade_test"]
- }),
- false,
- );
- let document = BASKET_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Trade"));
- assert!(rendered.contains("radroots trade submit trade_test"));
- assert!(!rendered.contains("order"));
- assert!(!rendered.contains("internal-draft-path"));
- }
-}
diff --git a/src/out/terminal/renderers/common.rs b/src/out/terminal/renderers/common.rs
@@ -4,10 +4,9 @@ use crate::out::envelope::OutputEnvelope;
use crate::out::terminal::actions::terminal_actions_from_next_actions;
use crate::out::terminal::errors::terminal_error_document;
use crate::out::terminal::layout::{
- TerminalDocument, TerminalField, TerminalHeader, TerminalReference, TerminalSection,
- TerminalSymbol, TerminalWarning,
+ TerminalDocument, TerminalField, TerminalHeader, TerminalReference, TerminalSymbol,
+ TerminalWarning,
};
-use crate::out::terminal::tables::{TerminalTable, TerminalTableColumn, TerminalTableRow};
use crate::out::terminal::values::{proof_summary, string_path, transport_label};
pub(crate) fn base_terminal_document(envelope: &OutputEnvelope) -> TerminalDocument {
@@ -36,43 +35,7 @@ pub(crate) fn base_terminal_document(envelope: &OutputEnvelope) -> TerminalDocum
document
}
-pub(crate) fn document_with_title(
- envelope: &OutputEnvelope,
- title: impl Into<String>,
-) -> TerminalDocument {
- let mut document = base_terminal_document(envelope);
- document.header.title = title.into();
- document
-}
-
-pub(crate) fn document_with_status_title(
- envelope: &OutputEnvelope,
- title_prefix: &str,
-) -> TerminalDocument {
- let status = terminal_envelope_status(envelope);
- let mut document = base_terminal_document(envelope);
- document.header.symbol = terminal_status_symbol(status, envelope.dry_run);
- document.header.title = format!("{title_prefix} {}", terminal_status_label(status));
- document
-}
-
-pub(crate) fn result(envelope: &OutputEnvelope) -> &Value {
- &envelope.result
-}
-
-pub(crate) fn display_source(envelope: &OutputEnvelope) -> &Value {
- terminal_display_source(envelope)
-}
-
-pub(crate) fn status_label(status: &str) -> String {
- terminal_status_label(status)
-}
-
-pub(crate) fn push_field(
- document: &mut TerminalDocument,
- label: impl Into<String>,
- value: impl Into<String>,
-) {
+fn push_field(document: &mut TerminalDocument, label: impl Into<String>, value: impl Into<String>) {
let label = label.into();
let value = value.into();
if value.trim().is_empty() {
@@ -88,149 +51,6 @@ pub(crate) fn push_field(
document.fields.push(TerminalField::new(label, value));
}
-pub(crate) fn push_verbose_field(
- document: &mut TerminalDocument,
- label: impl Into<String>,
- value: impl Into<String>,
-) {
- let value = value.into();
- if value.trim().is_empty() {
- return;
- }
- document.fields.push(TerminalField::verbose(label, value));
-}
-
-pub(crate) fn push_path_field(
- document: &mut TerminalDocument,
- label: &str,
- value: &Value,
- path: &[&str],
-) {
- if let Some(value) = string_path(value, path) {
- push_field(document, label, value);
- }
-}
-
-pub(crate) fn push_verbose_path_field(
- document: &mut TerminalDocument,
- label: &str,
- value: &Value,
- path: &[&str],
-) {
- if let Some(value) = string_path(value, path) {
- push_verbose_field(document, label, value);
- }
-}
-
-pub(crate) fn push_count_field(
- document: &mut TerminalDocument,
- label: &str,
- value: &Value,
- path: &[&str],
-) {
- if let Some(value) = number_label_path(value, path) {
- push_field(document, label, value);
- }
-}
-
-pub(crate) fn push_number_field(
- document: &mut TerminalDocument,
- label: &str,
- value: &Value,
- path: &[&str],
-) {
- if let Some(value) = number_label_path(value, path) {
- push_field(document, label, value);
- }
-}
-
-pub(crate) fn push_bool_field(
- document: &mut TerminalDocument,
- label: &str,
- value: &Value,
- path: &[&str],
-) {
- if let Some(value) = bool_path(value, path) {
- push_field(document, label, if value { "yes" } else { "no" });
- }
-}
-
-pub(crate) fn table_section(
- title: &str,
- columns: Vec<TerminalTableColumn>,
- rows: Vec<TerminalTableRow>,
- empty: &str,
-) -> TerminalSection {
- let mut table = TerminalTable::new(columns).with_empty(empty);
- for row in rows {
- table = table.with_row(row);
- }
- TerminalSection::table(title, table)
-}
-
-pub(crate) fn string(value: &Value, path: &[&str]) -> Option<String> {
- string_path(value, path).map(str::to_owned)
-}
-
-pub(crate) fn array<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Vec<Value>> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- current.as_array()
-}
-
-pub(crate) fn number_path(value: &Value, path: &[&str]) -> Option<i64> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- current.as_i64()
-}
-
-pub(crate) fn number_label_path(value: &Value, path: &[&str]) -> Option<String> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- number_label(current)
-}
-
-pub(crate) fn number_label(value: &Value) -> Option<String> {
- if let Some(value) = value.as_i64() {
- return Some(value.to_string());
- }
- if let Some(value) = value.as_u64() {
- return Some(value.to_string());
- }
- value
- .as_f64()
- .filter(|value| value.is_finite())
- .map(|value| {
- let mut rendered = format!("{value:.6}");
- while rendered.contains('.') && rendered.ends_with('0') {
- rendered.pop();
- }
- if rendered.ends_with('.') {
- rendered.pop();
- }
- rendered
- })
-}
-
-pub(crate) fn bool_path(value: &Value, path: &[&str]) -> Option<bool> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- current.as_bool()
-}
-
-pub(crate) fn title_for(envelope: &OutputEnvelope, noun: &str) -> String {
- let status = terminal_envelope_status(envelope);
- format!("{noun} {}", terminal_status_label(status))
-}
-
fn add_terminal_display_fields(
document: &mut TerminalDocument,
display: &Value,
diff --git a/src/out/terminal/renderers/config.rs b/src/out/terminal/renderers/config.rs
@@ -1,58 +0,0 @@
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::{TerminalDocument, TerminalSymbol};
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::values::transport_label;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry.register("config.get", &CONFIG_RENDERER)
-}
-
-struct ConfigRenderer;
-
-static CONFIG_RENDERER: ConfigRenderer = ConfigRenderer;
-
-impl TerminalOperationRenderer for ConfigRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::result(envelope);
- let publish_state =
- common::string(result, &["publish", "state"]).unwrap_or_else(|| "ready".to_owned());
- let mut document = common::document_with_title(
- envelope,
- format!("Config {}", common::status_label(publish_state.as_str())),
- );
- if !matches!(publish_state.as_str(), "ready" | "configured" | "ok") {
- document.header.symbol = TerminalSymbol::Attention;
- }
- common::push_path_field(&mut document, "Output", result, &["output", "format"]);
- if let Some(transport) = common::string(result, &["publish", "transport"]) {
- common::push_field(
- &mut document,
- "Transport",
- transport_label(transport.as_str()),
- );
- }
- common::push_path_field(&mut document, "Publish", result, &["publish", "state"]);
- common::push_path_field(&mut document, "Signer", result, &["signer", "mode"]);
- common::push_path_field(
- &mut document,
- "Account",
- result,
- &["account_resolution", "status"],
- );
- common::push_count_field(&mut document, "Relays", result, &["relay", "count"]);
- common::push_verbose_path_field(&mut document, "Profile", result, &["paths", "profile"]);
- common::push_verbose_path_field(
- &mut document,
- "Config",
- result,
- &["paths", "app_config_path"],
- );
- document
- }
-}
diff --git a/src/out/terminal/renderers/farm.rs b/src/out/terminal/renderers/farm.rs
@@ -1,368 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-use crate::out::terminal::values::{relay_summary, transport_label};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("farm.create", &FARM_RENDERER)
- .register("farm.get", &FARM_RENDERER)
- .register("farm.rebind", &FARM_RENDERER)
- .register("farm.profile.update", &FARM_RENDERER)
- .register("farm.location.set", &FARM_RENDERER)
- .register("farm.location.get", &FARM_RENDERER)
- .register("farm.location.clear", &FARM_RENDERER)
- .register("farm.fulfillment.update", &FARM_RENDERER)
- .register("farm.readiness.check", &FARM_RENDERER)
- .register("farm.publish", &FARM_RENDERER)
-}
-
-struct FarmRenderer;
-
-static FARM_RENDERER: FarmRenderer = FarmRenderer;
-
-impl TerminalOperationRenderer for FarmRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- match envelope.operation_id.as_str() {
- "farm.location.set" | "farm.location.get" | "farm.location.clear" => {
- farm_location_document(envelope, result)
- }
- "farm.readiness.check" => farm_readiness_document(envelope, result),
- "farm.publish" => farm_publish_document(envelope, result),
- "farm.get" => farm_get_document(envelope, result),
- "farm.rebind" => farm_rebind_document(envelope, result),
- "farm.profile.update" | "farm.fulfillment.update" => {
- farm_update_document(envelope, result)
- }
- _ => farm_create_document(envelope, result),
- }
- }
-}
-
-fn farm_create_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, farm_title(envelope.operation_id.as_str()));
- push_farm_summary_fields(&mut document, result);
- document
-}
-
-fn farm_get_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Farm");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Scope", result, &["scope"]);
- common::push_path_field(&mut document, "Path", result, &["path"]);
- push_farm_document_fields(&mut document, result);
- document
-}
-
-fn farm_rebind_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Farm rebound");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Scope", result, &["scope"]);
- common::push_path_field(&mut document, "Path", result, &["path"]);
- common::push_path_field(&mut document, "From", result, &["from_seller_account_id"]);
- common::push_path_field(&mut document, "To", result, &["to_seller_account_id"]);
- common::push_bool_field(
- &mut document,
- "Seller changed",
- result,
- &["seller_pubkey_changed"],
- );
- push_farm_summary_fields(&mut document, result);
- document
-}
-
-fn farm_update_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, farm_title(envelope.operation_id.as_str()));
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Field", result, &["field"]);
- common::push_path_field(&mut document, "Value", result, &["value"]);
- push_farm_summary_fields(&mut document, result);
- document
-}
-
-fn farm_location_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, farm_title(envelope.operation_id.as_str()));
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
- common::push_path_field(&mut document, "Label", result, &["label"]);
- common::push_number_field(
- &mut document,
- "Latitude",
- result,
- &["exact_location", "lat"],
- );
- common::push_number_field(
- &mut document,
- "Longitude",
- result,
- &["exact_location", "lng"],
- );
- common::push_path_field(
- &mut document,
- "Locality",
- result,
- &["public_locality", "primary"],
- );
- common::push_path_field(&mut document, "City", result, &["public_locality", "city"]);
- common::push_path_field(
- &mut document,
- "Region",
- result,
- &["public_locality", "region"],
- );
- common::push_path_field(
- &mut document,
- "Country",
- result,
- &["public_locality", "country"],
- );
- common::push_count_field(&mut document, "GeoNames", result, &["geonames_feature_id"]);
- common::push_bool_field(&mut document, "Cleared", result, &["cleared"]);
- common::push_verbose_path_field(
- &mut document,
- "GeoNames DB",
- result,
- &["geonames_database_path"],
- );
- push_location_candidates(&mut document, result);
- document
-}
-
-fn farm_readiness_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, common::title_for(envelope, "Farm readiness"));
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Scope", result, &["scope"]);
- common::push_path_field(&mut document, "Account", result, &["account_state"]);
- common::push_path_field(
- &mut document,
- "Listings",
- result,
- &["listing_defaults_state"],
- );
- common::push_path_field(&mut document, "Publish", result, &["publish_state"]);
- if let Some(mode) = common::string(result, &["transport_profile"]) {
- common::push_field(&mut document, "Transport", transport_label(mode.as_str()));
- }
- common::push_bool_field(&mut document, "Executable", result, &["publish_executable"]);
- push_farm_summary_fields(&mut document, result);
- push_missing_section(&mut document, result);
- document
-}
-
-fn farm_publish_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Farm published");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
- common::push_path_field(&mut document, "Account", result, &["seller_account_id"]);
- common::push_path_field(&mut document, "Profile", result, &["profile", "state"]);
- common::push_path_field(&mut document, "Farm publish", result, &["farm", "state"]);
- push_publish_component_fields(
- &mut document,
- "Profile targets",
- "Profile event",
- result,
- &["profile"],
- );
- push_publish_component_fields(
- &mut document,
- "Farm targets",
- "Farm event",
- result,
- &["farm"],
- );
- push_publish_components_section(&mut document, result);
- document
-}
-
-fn push_farm_summary_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(document, "State", result, &["state"]);
- common::push_path_field(document, "Source", result, &["source"]);
- common::push_path_field(document, "Scope", result, &["config", "scope"]);
- common::push_path_field(document, "Farm", result, &["config", "farm_d_tag"]);
- common::push_path_field(document, "Name", result, &["config", "name"]);
- common::push_path_field(
- document,
- "Location",
- result,
- &["config", "location_primary"],
- );
- common::push_path_field(document, "Delivery", result, &["config", "delivery_method"]);
- common::push_path_field(document, "Path", result, &["config", "path"]);
-}
-
-fn push_farm_document_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(
- document,
- "Farm",
- result,
- &["document", "selection", "farm_d_tag"],
- );
- common::push_path_field(
- document,
- "Account",
- result,
- &["document", "selection", "seller_account_id"],
- );
- common::push_path_field(document, "Name", result, &["document", "farm", "name"]);
- common::push_path_field(
- document,
- "Display",
- result,
- &["document", "profile", "display_name"],
- );
- common::push_path_field(
- document,
- "Location",
- result,
- &["document", "listing_defaults", "location", "primary"],
- );
- common::push_path_field(
- document,
- "Delivery",
- result,
- &["document", "listing_defaults", "delivery_method"],
- );
-}
-
-fn push_location_candidates(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["candidates"])
- .into_iter()
- .flatten()
- .map(|candidate| {
- TerminalTableRow::new(vec![
- common::number_label_path(candidate, &["geonames_feature_id"]).unwrap_or_default(),
- common::string(candidate, &["display_name"]).unwrap_or_default(),
- common::number_label_path(candidate, &["exact_location", "lat"])
- .unwrap_or_default(),
- common::number_label_path(candidate, &["exact_location", "lng"])
- .unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "GeoNames candidates",
- vec![
- TerminalTableColumn::new("ID", 2, 10),
- TerminalTableColumn::new("Name", 8, 32),
- TerminalTableColumn::new("Lat", 3, 10),
- TerminalTableColumn::new("Lng", 3, 11),
- ],
- rows,
- "No GeoNames candidates",
- ));
-}
-
-fn push_missing_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["missing"])
- .into_iter()
- .flatten()
- .filter_map(Value::as_str)
- .map(|value| TerminalTableRow::new(vec![value.to_owned()]))
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Missing",
- vec![TerminalTableColumn::new("Item", 8, 48)],
- rows,
- "No missing requirements",
- ));
-}
-
-fn push_publish_component_fields(
- document: &mut TerminalDocument,
- target_label: &str,
- event_label: &str,
- result: &Value,
- path: &[&str],
-) {
- let Some(component) = nested_value(result, path) else {
- return;
- };
- if let Some(summary) = relay_component_summary(component) {
- common::push_field(document, target_label, summary);
- }
- common::push_path_field(document, event_label, component, &["event_id"]);
- common::push_path_field(document, "Job", component, &["job_id"]);
-}
-
-fn push_publish_components_section(document: &mut TerminalDocument, result: &Value) {
- let rows = ["profile", "farm"]
- .into_iter()
- .filter_map(|component_name| {
- let component = result.get(component_name)?;
- Some(TerminalTableRow::new(vec![
- component_name.to_owned(),
- common::string(component, &["state"]).unwrap_or_default(),
- relay_component_summary(component).unwrap_or_default(),
- ]))
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Publish components",
- vec![
- TerminalTableColumn::new("Part", 4, 8),
- TerminalTableColumn::new("State", 5, 16),
- TerminalTableColumn::new("Targets", 8, 28),
- ],
- rows,
- "No publish components",
- ));
-}
-
-fn relay_component_summary(value: &Value) -> Option<String> {
- let acknowledged = common::array(value, &["accepted_transport_endpoints"])
- .map(Vec::len)
- .unwrap_or(0);
- let failed = common::array(value, &["failed_transport_targets"])
- .map(Vec::len)
- .unwrap_or(0);
- let target = common::array(value, &["target_transport_endpoints"])
- .map(Vec::len)
- .unwrap_or(0);
- (acknowledged > 0 || failed > 0 || target > 0)
- .then(|| relay_summary(acknowledged, failed, "acknowledged"))
-}
-
-fn nested_value<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- Some(current)
-}
-
-fn farm_title(operation_id: &str) -> &'static str {
- match operation_id {
- "farm.create" => "Farm created",
- "farm.rebind" => "Farm rebound",
- "farm.profile.update" => "Farm profile updated",
- "farm.location.set" => "Farm location set",
- "farm.location.get" => "Farm location",
- "farm.location.clear" => "Farm location cleared",
- "farm.fulfillment.update" => "Farm fulfillment updated",
- "farm.publish" => "Farm published",
- _ => "Farm",
- }
-}
diff --git a/src/out/terminal/renderers/health.rs b/src/out/terminal/renderers/health.rs
@@ -1,55 +0,0 @@
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("health.status.get", &HEALTH_RENDERER)
- .register("health.check.run", &HEALTH_RENDERER)
-}
-
-struct HealthRenderer;
-
-static HEALTH_RENDERER: HealthRenderer = HealthRenderer;
-
-impl TerminalOperationRenderer for HealthRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::result(envelope);
- let title = match envelope.operation_id.as_str() {
- "health.check.run" => common::title_for(envelope, "Health check"),
- _ => common::title_for(envelope, "Health"),
- };
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(
- &mut document,
- "Account",
- result,
- &["checks", "account", "state"],
- );
- common::push_path_field(
- &mut document,
- "Store",
- result,
- &["checks", "store", "state"],
- );
- common::push_path_field(
- &mut document,
- "Publish",
- result,
- &["checks", "publish", "state"],
- );
- common::push_path_field(
- &mut document,
- "Network",
- result,
- &["checks", "network", "state"],
- );
- document
- }
-}
diff --git a/src/out/terminal/renderers/listing.rs b/src/out/terminal/renderers/listing.rs
@@ -1,382 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-use crate::out::terminal::values::{proof_summary, relay_summary, transport_label};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("listing.create", &LISTING_RENDERER)
- .register("listing.get", &LISTING_RENDERER)
- .register("listing.list", &LISTING_RENDERER)
- .register("listing.app.list", &LISTING_RENDERER)
- .register("listing.app.export", &LISTING_RENDERER)
- .register("listing.update", &LISTING_RENDERER)
- .register("listing.validate", &LISTING_RENDERER)
- .register("listing.rebind", &LISTING_RENDERER)
- .register("listing.publish", &LISTING_RENDERER)
- .register("listing.archive", &LISTING_RENDERER)
-}
-
-struct ListingRenderer;
-
-static LISTING_RENDERER: ListingRenderer = ListingRenderer;
-
-impl TerminalOperationRenderer for ListingRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- match envelope.operation_id.as_str() {
- "listing.list" => listing_list_document(envelope, result),
- "listing.app.list" => listing_app_list_document(envelope, result),
- "listing.app.export" => listing_app_export_document(envelope, result),
- "listing.validate" => listing_validate_document(envelope, result),
- "listing.rebind" => listing_rebind_document(envelope, result),
- "listing.publish" => listing_publish_document(envelope, result),
- "listing.archive" | "listing.update" => listing_mutation_document(envelope, result),
- "listing.get" => listing_get_document(envelope, result),
- _ => listing_create_document(envelope, result),
- }
- }
-}
-
-fn listing_create_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listing draft created");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "Key", result, &["key"]);
- common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
- common::push_path_field(&mut document, "Delivery", result, &["delivery_method"]);
- common::push_path_field(&mut document, "Location", result, &["location_primary"]);
- common::push_path_field(&mut document, "File", result, &["file"]);
- document
-}
-
-fn listing_get_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listing");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "Key", result, &["product_key"]);
- common::push_path_field(&mut document, "Title", result, &["title"]);
- common::push_path_field(&mut document, "Category", result, &["category"]);
- common::push_path_field(&mut document, "Location", result, &["location_primary"]);
- common::push_path_field(&mut document, "Available", result, &["available", "label"]);
- common::push_path_field(&mut document, "Currency", result, &["price", "currency"]);
- common::push_number_field(&mut document, "Price", result, &["price", "amount"]);
- document
-}
-
-fn listing_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listings");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- common::push_verbose_path_field(&mut document, "Drafts", result, &["draft_dir"]);
- let rows = common::array(result, &["listings"])
- .into_iter()
- .flatten()
- .map(|listing| {
- TerminalTableRow::new(vec![
- common::string(listing, &["id"]).unwrap_or_default(),
- common::string(listing, &["title"])
- .or_else(|| common::string(listing, &["product_key"]))
- .unwrap_or_default(),
- common::string(listing, &["state"]).unwrap_or_default(),
- common::string(listing, &["file"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Listings",
- vec![
- TerminalTableColumn::new("Listing", 8, 22),
- TerminalTableColumn::new("Title", 5, 24),
- TerminalTableColumn::new("State", 5, 12),
- TerminalTableColumn::new("File", 4, 34),
- ],
- rows,
- "No listing drafts found",
- ));
- document
-}
-
-fn listing_app_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listing app records");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- common::push_bool_field(&mut document, "More", result, &["has_more"]);
- let rows = common::array(result, &["records"])
- .into_iter()
- .flatten()
- .map(|record| {
- TerminalTableRow::new(vec![
- common::string(record, &["record_id"]).unwrap_or_default(),
- common::string(record, &["title"])
- .or_else(|| common::string(record, &["listing_id"]))
- .unwrap_or_default(),
- common::string(record, &["status"]).unwrap_or_default(),
- common::bool_path(record, &["exportable"])
- .map(|value| if value { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Records",
- vec![
- TerminalTableColumn::new("Record", 8, 22),
- TerminalTableColumn::new("Listing", 8, 24),
- TerminalTableColumn::new("Status", 6, 14),
- TerminalTableColumn::new("Export", 6, 6),
- ],
- rows,
- "No app-authored listing records found",
- ));
- document
-}
-
-fn listing_app_export_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listing app export");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Record", result, &["record_id"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
- common::push_bool_field(&mut document, "Valid", result, &["valid"]);
- common::push_path_field(&mut document, "File", result, &["file"]);
- push_issues_section(&mut document, result);
- document
-}
-
-fn listing_validate_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = if common::bool_path(result, &["valid"]) == Some(true) {
- "Listing valid"
- } else {
- "Listing validation"
- };
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_bool_field(&mut document, "Valid", result, &["valid"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
- common::push_path_field(&mut document, "File", result, &["file"]);
- push_issues_section(&mut document, result);
- document
-}
-
-fn listing_rebind_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listing rebound");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "From", result, &["from_seller_account_id"]);
- common::push_path_field(&mut document, "To", result, &["to_seller_account_id"]);
- common::push_path_field(&mut document, "Farm", result, &["to_farm_d_tag"]);
- common::push_bool_field(
- &mut document,
- "Seller changed",
- result,
- &["seller_pubkey_changed"],
- );
- common::push_bool_field(
- &mut document,
- "Address changed",
- result,
- &["listing_addr_changed"],
- );
- common::push_path_field(&mut document, "File", result, &["file"]);
- document
-}
-
-fn listing_mutation_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document =
- common::document_with_title(envelope, listing_title(envelope.operation_id.as_str()));
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "Address", result, &["listing_addr"]);
- common::push_path_field(&mut document, "Account", result, &["seller_account_id"]);
- common::push_path_field(&mut document, "Event", result, &["event_id"]);
- common::push_path_field(&mut document, "File", result, &["file"]);
- push_relay_field(&mut document, result);
- document
-}
-
-fn listing_publish_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Listing published");
- if let Some(listing) = first_string(
- result,
- &[
- &["listing_id"],
- &["listing", "id"],
- &["listing", "d_tag"],
- &["resource", "id"],
- &["listing_addr"],
- &["listing_address"],
- ],
- ) {
- common::push_field(&mut document, "Listing", listing);
- }
- if let Some(mode) = first_string(
- result,
- &[
- &["publish", "mode"],
- &["transport_profile"],
- &["transport"],
- &["source"],
- ],
- ) {
- common::push_field(&mut document, "Transport", transport_label(mode.as_str()));
- }
- push_relay_field(&mut document, result);
- if let Some(event_id) = first_string(
- result,
- &[
- &["event_id"],
- &["publish", "event_id"],
- &["event", "id"],
- &["event", "event_id"],
- &["receipt", "event_id"],
- ],
- ) {
- common::push_field(&mut document, "Event", event_id);
- }
- if let Some(proof) = proof_summary(result) {
- common::push_field(&mut document, "Proof", proof);
- }
- document
-}
-
-fn push_issues_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["issues"])
- .into_iter()
- .flatten()
- .map(|issue| {
- TerminalTableRow::new(vec![
- common::string(issue, &["field"]).unwrap_or_default(),
- common::string(issue, &["message"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Issues",
- vec![
- TerminalTableColumn::new("Field", 5, 18),
- TerminalTableColumn::new("Message", 7, 54),
- ],
- rows,
- "No validation issues",
- ));
-}
-
-fn push_relay_field(document: &mut TerminalDocument, result: &Value) {
- if let Some(summary) = relay_field_summary(result) {
- common::push_field(document, "Targets", summary);
- }
-}
-
-fn relay_field_summary(result: &Value) -> Option<String> {
- let acknowledged = common::array(result, &["accepted_transport_endpoints"])
- .map(Vec::len)
- .or_else(|| {
- common::number_path(result, &["publish", "acknowledged_count"])
- .and_then(|value| usize::try_from(value).ok())
- })
- .or_else(|| {
- common::number_path(result, &["receipt", "acknowledged_count"])
- .and_then(|value| usize::try_from(value).ok())
- })
- .unwrap_or(0);
- let failed = common::array(result, &["failed_transport_targets"])
- .map(Vec::len)
- .or_else(|| {
- common::number_path(result, &["publish", "failed_count"])
- .and_then(|value| usize::try_from(value).ok())
- })
- .or_else(|| {
- common::number_path(result, &["receipt", "failed_count"])
- .and_then(|value| usize::try_from(value).ok())
- })
- .unwrap_or(0);
- let target = common::array(result, &["target_transport_endpoints"])
- .map(Vec::len)
- .unwrap_or(0);
- (acknowledged > 0 || failed > 0 || target > 0)
- .then(|| relay_summary(acknowledged, failed, "acknowledged"))
-}
-
-fn first_string(value: &Value, paths: &[&[&str]]) -> Option<String> {
- paths.iter().find_map(|path| common::string(value, path))
-}
-
-fn listing_title(operation_id: &str) -> &'static str {
- match operation_id {
- "listing.update" => "Listing updated",
- "listing.archive" => "Listing archived",
- _ => "Listing",
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::out::envelope::EnvelopeContext;
- use crate::out::terminal::renderer::render_terminal_document;
- use serde_json::json;
-
- use super::*;
-
- #[test]
- fn publish_receipt_omits_absent_proof_source() {
- let envelope = OutputEnvelope::success(
- "listing.publish",
- json!({
- "state": "published",
- "listing_id": "AAAAAAAAAAAAAAAAAAAAAg",
- "source": "SDK listing publish · configured signer",
- "accepted_transport_endpoints": ["wss://relay.example"],
- "failed_transport_targets": [],
- "event_id": "9f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac12",
- "actions": ["radroots listing get AAAAAAAAAAAAAAAAAAAAAg"]
- }),
- EnvelopeContext::new("req_listing_publish", false),
- );
- let document = LISTING_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("✓ Listing published"));
- assert!(rendered.contains("Listing"));
- assert!(rendered.contains("Targets"));
- assert!(rendered.contains("Event"));
- assert!(!rendered.contains("Proof"));
- assert!(!rendered.contains("verified"));
- }
-
- #[test]
- fn publish_receipt_requires_verified_true_for_verified_claim() {
- let envelope = OutputEnvelope::success(
- "listing.publish",
- json!({
- "state": "published",
- "listing_id": "AAAAAAAAAAAAAAAAAAAAAg",
- "proof_verification": {
- "state": "valid",
- "cryptographic_proof_verified": false,
- "proof_system": "SP1"
- }
- }),
- EnvelopeContext::new("req_listing_publish", false),
- );
- let document = LISTING_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Proof"));
- assert!(!rendered.contains("Proof verified"));
- assert!(!rendered.contains("verified ·"));
- }
-}
diff --git a/src/out/terminal/renderers/market.rs b/src/out/terminal/renderers/market.rs
@@ -1,216 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::{TerminalDocument, TerminalSection};
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-use crate::out::terminal::values::{price_label, quantity_label};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("market.refresh", &MARKET_RENDERER)
- .register("market.product.search", &MARKET_RENDERER)
- .register("market.listing.get", &MARKET_RENDERER)
-}
-
-struct MarketRenderer;
-
-static MARKET_RENDERER: MarketRenderer = MarketRenderer;
-
-impl TerminalOperationRenderer for MarketRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- match envelope.operation_id.as_str() {
- "market.product.search" => market_search_document(envelope, result),
- "market.listing.get" => market_listing_document(envelope, result),
- _ => market_refresh_document(envelope, result),
- }
- }
-}
-
-fn market_refresh_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Market refresh");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(
- &mut document,
- "Freshness",
- result,
- &["freshness", "display"],
- );
- common::push_count_field(&mut document, "Relays", result, &["relay_count"]);
- common::push_count_field(&mut document, "Fetched", result, &["fetched_count"]);
- common::push_count_field(&mut document, "Ingested", result, &["ingested_count"]);
- common::push_count_field(&mut document, "Skipped", result, &["skipped_count"]);
- common::push_count_field(&mut document, "Failed", result, &["failed_count"]);
- common::push_path_field(&mut document, "Replica", result, &["replica_store"]);
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- document
-}
-
-fn market_search_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Market search");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Query", result, &["query"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- common::push_path_field(
- &mut document,
- "Freshness",
- result,
- &["freshness", "display"],
- );
- common::push_count_field(&mut document, "Relays", result, &["relay_count"]);
- let rows = common::array(result, &["results"])
- .into_iter()
- .flatten()
- .map(|entry| {
- TerminalTableRow::new(vec![
- common::string(entry, &["product_key"])
- .or_else(|| common::string(entry, &["id"]))
- .unwrap_or_default(),
- common::string(entry, &["title"]).unwrap_or_default(),
- quantity(entry, &["available"]),
- price(entry, &["price"]),
- checkout_label(entry),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Results",
- vec![
- TerminalTableColumn::new("Listing", 8, 18),
- TerminalTableColumn::new("Title", 5, 22),
- TerminalTableColumn::new("Available", 9, 16),
- TerminalTableColumn::new("Price", 5, 18),
- TerminalTableColumn::new("Checkout", 8, 8),
- ],
- rows,
- "No matching market listings",
- ));
- if let Some(hyf) = result.get("hyf") {
- common::push_verbose_path_field(&mut document, "HYF", hyf, &["state"]);
- common::push_verbose_path_field(&mut document, "Rewritten", hyf, &["rewritten_query"]);
- }
- document
-}
-
-fn market_listing_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Market listing");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
- common::push_path_field(&mut document, "Key", result, &["product_key"]);
- common::push_path_field(&mut document, "Title", result, &["title"]);
- common::push_path_field(&mut document, "Category", result, &["category"]);
- common::push_path_field(&mut document, "Location", result, &["location_primary"]);
- common::push_path_field(&mut document, "Primary bin", result, &["primary_bin_id"]);
- common::push_field(&mut document, "Available", quantity(result, &["available"]));
- common::push_field(&mut document, "Price", price(result, &["price"]));
- common::push_field(&mut document, "Checkout", checkout_label(result));
- common::push_path_field(
- &mut document,
- "Freshness",
- result,
- &["provenance", "freshness"],
- );
- push_reason_codes(&mut document, result);
- document
-}
-
-fn push_reason_codes(document: &mut TerminalDocument, result: &Value) {
- let lines = common::array(result, &["reason_codes"])
- .into_iter()
- .flatten()
- .filter_map(Value::as_str)
- .map(str::to_owned)
- .collect::<Vec<_>>();
- if !lines.is_empty() {
- document
- .sections
- .push(TerminalSection::lines("Reasons", lines));
- }
-}
-
-fn quantity(value: &Value, path: &[&str]) -> String {
- let Some(quantity) = nested(value, path) else {
- return String::new();
- };
- common::string(quantity, &["label"]).unwrap_or_else(|| {
- let amount = common::number_label_path(quantity, &["total_amount"])
- .or_else(|| common::number_label_path(quantity, &["available_amount"]))
- .unwrap_or_default();
- let unit = common::string(quantity, &["total_unit"]);
- quantity_label(amount, unit.as_deref())
- })
-}
-
-fn price(value: &Value, path: &[&str]) -> String {
- let Some(price) = nested(value, path) else {
- return String::new();
- };
- let amount = common::number_label_path(price, &["amount"]).unwrap_or_default();
- let currency = common::string(price, &["currency"]).unwrap_or_default();
- let unit = common::string(price, &["per_unit"]);
- if currency.is_empty() {
- return amount;
- }
- price_label(amount, currency.as_str(), unit.as_deref())
-}
-
-fn checkout_label(value: &Value) -> String {
- common::bool_path(value, &["checkout_enabled"])
- .map(|enabled| {
- if enabled {
- "ready".to_owned()
- } else {
- "blocked".to_owned()
- }
- })
- .unwrap_or_default()
-}
-
-fn nested<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- Some(current)
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::json;
-
- use crate::out::envelope::{EnvelopeContext, next_actions_from_result_value};
- use crate::out::terminal::renderer::render_terminal_document;
-
- use super::*;
-
- #[test]
- fn empty_search_renders_stable_empty_table() {
- let mut envelope = OutputEnvelope::success(
- "market.product.search",
- json!({
- "state": "empty",
- "query": "eggs",
- "count": 0,
- "relay_count": 0,
- "freshness": { "display": "missing" },
- "results": [],
- "actions": ["radroots market refresh"]
- }),
- EnvelopeContext::new("req_market_search", false),
- );
- envelope.next_actions = next_actions_from_result_value(&envelope.result);
- let document = MARKET_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Market search"));
- assert!(rendered.contains("No matching market listings"));
- assert!(rendered.contains("radroots market refresh"));
- }
-}
diff --git a/src/out/terminal/renderers/mod.rs b/src/out/terminal/renderers/mod.rs
@@ -1,14 +1,2 @@
-pub mod account;
-pub mod basket;
pub mod common;
-pub mod config;
-pub mod farm;
-pub mod health;
-pub mod listing;
-pub mod market;
-pub mod runtime;
-pub mod store;
-pub mod sync;
-pub mod trade;
-pub mod validation;
-pub mod workspace;
+pub mod v1;
diff --git a/src/out/terminal/renderers/runtime.rs b/src/out/terminal/renderers/runtime.rs
@@ -1,276 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("signer.status.get", &RUNTIME_RENDERER)
- .register("transport.profile.get", &RUNTIME_RENDERER)
- .register("transport.profile.set", &RUNTIME_RENDERER)
- .register("transport.status", &RUNTIME_RENDERER)
- .register("transport.outbox.status", &RUNTIME_RENDERER)
- .register("transport.outbox.push", &RUNTIME_RENDERER)
- .register("mesh.scope.get", &RUNTIME_RENDERER)
- .register("mesh.scope.set", &RUNTIME_RENDERER)
- .register("mesh.status", &RUNTIME_RENDERER)
- .register("mesh.policy.check", &RUNTIME_RENDERER)
-}
-
-struct RuntimeRenderer;
-
-static RUNTIME_RENDERER: RuntimeRenderer = RuntimeRenderer;
-
-impl TerminalOperationRenderer for RuntimeRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::result(envelope);
- match envelope.operation_id.as_str() {
- "transport.profile.get" | "transport.profile.set" => {
- transport_profile_document(envelope, result)
- }
- "transport.status" => transport_status_document(envelope, result),
- "transport.outbox.status" => transport_outbox_status_document(envelope, result),
- "transport.outbox.push" => transport_outbox_push_document(envelope, result),
- "mesh.scope.get" | "mesh.scope.set" => mesh_scope_document(envelope, result),
- "mesh.status" | "mesh.policy.check" => mesh_status_document(envelope, result),
- _ => signer_document(envelope, result),
- }
- }
-}
-
-fn signer_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Signer");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "Mode", result, &["mode"]);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(
- &mut document,
- "Account",
- result,
- &["account_resolution", "status"],
- );
- common::push_path_field(&mut document, "Binding", result, &["binding", "state"]);
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- if let Some(write_kinds) = common::array(result, &["write_kinds"]) {
- let rows = write_kinds
- .iter()
- .map(|write_kind| {
- TerminalTableRow::new(vec![
- common::string(write_kind, &["command"]).unwrap_or_default(),
- common::string(write_kind, &["event_kind"]).unwrap_or_else(|| {
- write_kind
- .get("event_kind")
- .and_then(Value::as_i64)
- .map(|value| value.to_string())
- .unwrap_or_default()
- }),
- common::bool_path(write_kind, &["ready"])
- .map(|ready| if ready { "ready" } else { "blocked" }.to_owned())
- .unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Write permissions",
- vec![
- TerminalTableColumn::new("Command", 7, 24),
- TerminalTableColumn::new("Kind", 4, 8),
- TerminalTableColumn::new("State", 5, 8),
- ],
- rows,
- "No signer write permissions reported",
- ));
- }
- document
-}
-
-fn transport_profile_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Transport Profile");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Source", result, &["source"]);
- common::push_path_field(&mut document, "Profile", result, &["profile_id"]);
- common::push_path_field(&mut document, "Profile kind", result, &["profile_kind"]);
- common::push_path_field(&mut document, "Configured", result, &["configured_state"]);
- common::push_bool_field(
- &mut document,
- "Delivery usable",
- result,
- &["profile_delivery_usable"],
- );
- common::push_path_field(&mut document, "Message", result, &["message"]);
- if let Some(relays) = common::array(result, &["nostr_relays"]) {
- let rows = relays
- .iter()
- .map(|relay| TerminalTableRow::new(vec![relay.as_str().unwrap_or_default().to_owned()]))
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Nostr relays",
- vec![TerminalTableColumn::new("URL", 12, 48)],
- rows,
- "No Nostr relays configured",
- ));
- }
- push_transport_status_table(&mut document, result, &["transport_statuses"]);
- document
-}
-
-fn transport_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Transport Status");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Source", result, &["source"]);
- common::push_path_field(
- &mut document,
- "Profile",
- result,
- &["active_profile", "profile_id"],
- );
- common::push_path_field(
- &mut document,
- "Profile kind",
- result,
- &["active_profile", "profile_kind"],
- );
- common::push_path_field(
- &mut document,
- "Configured",
- result,
- &["active_profile", "configured_state"],
- );
- common::push_bool_field(
- &mut document,
- "Delivery usable",
- result,
- &["active_profile", "profile_delivery_usable"],
- );
- common::push_path_field(
- &mut document,
- "Message",
- result,
- &["active_profile", "message"],
- );
- push_transport_status_table(&mut document, result, &["transports"]);
- document
-}
-
-fn push_transport_status_table(document: &mut TerminalDocument, result: &Value, path: &[&str]) {
- let rows = common::array(result, path)
- .into_iter()
- .flatten()
- .map(|transport| {
- TerminalTableRow::new(vec![
- common::string(transport, &["transport"]).unwrap_or_default(),
- common::string(transport, &["profile_id"]).unwrap_or_default(),
- common::bool_path(transport, &["configured"])
- .map(|value| if value { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- common::string(transport, &["implementation"]).unwrap_or_default(),
- common::bool_path(transport, &["usable_for_delivery"])
- .map(|value| if value { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- common::bool_path(transport, &["capabilities", "deliver"])
- .map(|value| if value { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- common::bool_path(transport, &["capabilities", "fetch"])
- .map(|value| if value { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- common::string(transport, &["endpoint_uri"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Transports",
- vec![
- TerminalTableColumn::new("Kind", 4, 12),
- TerminalTableColumn::new("Profile", 7, 18),
- TerminalTableColumn::new("Configured", 10, 10),
- TerminalTableColumn::new("Implementation", 14, 24),
- TerminalTableColumn::new("Usable", 6, 6),
- TerminalTableColumn::new("Deliver", 7, 7),
- TerminalTableColumn::new("Fetch", 5, 5),
- TerminalTableColumn::new("Endpoint", 8, 32),
- ],
- rows,
- "No transports reported",
- ));
-}
-
-fn transport_outbox_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Transport Outbox");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Profile", result, &["transport_profile"]);
- common::push_count_field(&mut document, "Total", result, &["total_count"]);
- common::push_count_field(&mut document, "Pending", result, &["pending_count"]);
- common::push_count_field(&mut document, "Retryable", result, &["retryable_count"]);
- common::push_count_field(&mut document, "Terminal", result, &["terminal_count"]);
- common::push_count_field(
- &mut document,
- "Deferred",
- result,
- &["deferred_until_implemented_count"],
- );
- common::push_count_field(
- &mut document,
- "Deferred",
- result,
- &["deferred_until_implemented_count"],
- );
- common::push_count_field(
- &mut document,
- "Ready signed",
- result,
- &["ready_signed_count"],
- );
- common::push_count_field(&mut document, "Publishing", result, &["publishing_count"]);
- common::push_path_field(&mut document, "Last error", result, &["last_error"]);
- document
-}
-
-fn transport_outbox_push_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Transport Outbox Push");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Attempted", result, &["attempted_events"]);
- common::push_count_field(&mut document, "Published", result, &["published_events"]);
- common::push_count_field(&mut document, "Retryable", result, &["retryable_events"]);
- common::push_count_field(&mut document, "Terminal", result, &["terminal_events"]);
- common::push_count_field(&mut document, "Targets", result, &["target_count"]);
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- document
-}
-
-fn mesh_scope_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Mesh Scope");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Scope", result, &["scope"]);
- common::push_path_field(&mut document, "Implementation", result, &["implementation"]);
- common::push_path_field(&mut document, "Message", result, &["message"]);
- document
-}
-
-fn mesh_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Mesh");
- let mut document = common::document_with_title(envelope, title);
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Scope", result, &["scope"]);
- common::push_path_field(&mut document, "Transport", result, &["transport"]);
- common::push_bool_field(&mut document, "Configured", result, &["configured"]);
- common::push_path_field(&mut document, "Implementation", result, &["implementation"]);
- common::push_bool_field(&mut document, "Usable", result, &["usable_for_delivery"]);
- common::push_path_field(&mut document, "Decision", result, &["decision"]);
- common::push_path_field(&mut document, "Deny Reason", result, &["deny_reason"]);
- common::push_path_field(&mut document, "Compression", result, &["compression"]);
- common::push_path_field(&mut document, "Message", result, &["message"]);
- document
-}
diff --git a/src/out/terminal/renderers/store.rs b/src/out/terminal/renderers/store.rs
@@ -1,62 +0,0 @@
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("store.init", &STORE_RENDERER)
- .register("store.status.get", &STORE_RENDERER)
- .register("store.export", &STORE_RENDERER)
- .register("store.backup.create", &STORE_RENDERER)
- .register("store.backup.restore", &STORE_RENDERER)
-}
-
-struct StoreRenderer;
-
-static STORE_RENDERER: StoreRenderer = StoreRenderer;
-
-impl TerminalOperationRenderer for StoreRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::result(envelope);
- let mut document =
- common::document_with_title(envelope, store_title(envelope.operation_id.as_str()));
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(&mut document, "Source", result, &["source"]);
- common::push_path_field(&mut document, "Storage", result, &["sdk_storage"]);
- common::push_path_field(
- &mut document,
- "Derived",
- result,
- &["derived_projection", "state"],
- );
- common::push_count_field(
- &mut document,
- "Events",
- result,
- &["event_store", "total_events"],
- );
- common::push_count_field(&mut document, "Outbox", result, &["outbox", "total_events"]);
- common::push_verbose_path_field(&mut document, "Root", result, &["local_root"]);
- common::push_verbose_path_field(&mut document, "SDK root", result, &["sdk_root"]);
- common::push_verbose_path_field(&mut document, "Export", result, &["path"]);
- common::push_verbose_path_field(&mut document, "Backup", result, &["backup_path"]);
- document
- }
-}
-
-fn store_title(operation_id: &str) -> &'static str {
- match operation_id {
- "store.init" => "Store initialized",
- "store.status.get" => "Store status",
- "store.export" => "Store exported",
- "store.backup.create" => "Store backup created",
- "store.backup.restore" => "Store backup restored",
- _ => "Store",
- }
-}
diff --git a/src/out/terminal/renderers/sync.rs b/src/out/terminal/renderers/sync.rs
@@ -1,71 +0,0 @@
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("sync.status.get", &SYNC_RENDERER)
- .register("sync.pull", &SYNC_RENDERER)
- .register("sync.push", &SYNC_RENDERER)
- .register("sync.watch", &SYNC_RENDERER)
-}
-
-struct SyncRenderer;
-
-static SYNC_RENDERER: SyncRenderer = SyncRenderer;
-
-impl TerminalOperationRenderer for SyncRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- let mut document =
- common::document_with_title(envelope, sync_title(envelope.operation_id.as_str()));
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_path_field(
- &mut document,
- "Freshness",
- result,
- &["freshness", "display"],
- );
- common::push_count_field(
- &mut document,
- "Transport targets",
- result,
- &["configured_transport_target_count"],
- );
- common::push_count_field(&mut document, "Fetched", result, &["fetched_count"]);
- common::push_count_field(&mut document, "Ingested", result, &["ingested_count"]);
- common::push_count_field(&mut document, "Skipped", result, &["skipped_count"]);
- common::push_count_field(&mut document, "Failed", result, &["failed_count"]);
- common::push_count_field(
- &mut document,
- "Pending",
- result,
- &["queue", "pending_count"],
- );
- common::push_count_field(
- &mut document,
- "Ready",
- result,
- &["queue", "ready_signed_count"],
- );
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- common::push_verbose_path_field(&mut document, "Root", result, &["local_root"]);
- document
- }
-}
-
-fn sync_title(operation_id: &str) -> &'static str {
- match operation_id {
- "sync.status.get" => "Sync status",
- "sync.pull" => "Sync pull",
- "sync.push" => "Sync push",
- "sync.watch" => "Sync watch",
- _ => "Sync",
- }
-}
diff --git a/src/out/terminal/renderers/trade.rs b/src/out/terminal/renderers/trade.rs
@@ -1,658 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-use crate::out::terminal::values::{ready_blocked, relay_summary, yes_no};
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("trade.submit", &TRADE_RENDERER)
- .register("trade.get", &TRADE_RENDERER)
- .register("trade.list", &TRADE_RENDERER)
- .register("trade.app.list", &TRADE_RENDERER)
- .register("trade.app.export", &TRADE_RENDERER)
- .register("trade.rebind", &TRADE_RENDERER)
- .register("trade.accept", &TRADE_RENDERER)
- .register("trade.decline", &TRADE_RENDERER)
- .register("trade.cancel", &TRADE_RENDERER)
- .register("trade.status.get", &TRADE_RENDERER)
- .register("trade.event.list", &TRADE_RENDERER)
- .register("trade.event.watch", &TRADE_RENDERER)
-}
-
-struct TradeRenderer;
-
-static TRADE_RENDERER: TradeRenderer = TradeRenderer;
-
-impl TerminalOperationRenderer for TradeRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- match envelope.operation_id.as_str() {
- "trade.list" => trade_list_document(envelope, result),
- "trade.app.list" => trade_app_list_document(envelope, result),
- "trade.app.export" => trade_app_export_document(envelope, result),
- "trade.get" => trade_detail_document(envelope, result),
- "trade.rebind" => trade_rebind_document(envelope, result),
- "trade.status.get" => trade_status_document(envelope, result),
- "trade.event.list" => trade_event_list_document(envelope, result),
- "trade.event.watch" => trade_event_watch_document(envelope, result),
- "trade.submit" => trade_publish_document(envelope, result),
- "trade.accept" | "trade.decline" => trade_decision_document(envelope, result),
- "trade.cancel" => trade_cancel_document(envelope, result),
- _ => trade_detail_document(envelope, result),
- }
- }
-}
-
-fn trade_publish_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Trade submit");
- push_trade_identity_fields(&mut document, result);
- common::push_bool_field(&mut document, "Dry run", result, &["dry_run"]);
- common::push_bool_field(&mut document, "Deduplicated", result, &["deduplicated"]);
- common::push_path_field(&mut document, "Event", result, &["event_id"]);
- common::push_path_field(&mut document, "Kind", result, &["event_kind"]);
- common::push_path_field(&mut document, "Signer", result, &["signer_mode"]);
- push_relay_field(&mut document, result);
- push_job_fields(&mut document, result);
- push_trade_items_section(&mut document, result);
- push_issue_sections(&mut document, result);
- document
-}
-
-fn trade_decision_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(
- envelope,
- trade_decision_title(envelope.operation_id.as_str()),
- );
- push_trade_identity_fields(&mut document, result);
- common::push_path_field(&mut document, "Decision", result, &["decision"]);
- common::push_bool_field(&mut document, "Dry run", result, &["dry_run"]);
- common::push_path_field(&mut document, "Request", result, &["request_event_id"]);
- common::push_path_field(&mut document, "Event", result, &["event_id"]);
- common::push_path_field(&mut document, "Kind", result, &["event_kind"]);
- common::push_path_field(&mut document, "Signer", result, &["signer_mode"]);
- push_relay_field(&mut document, result);
- push_inventory_section(&mut document, result);
- push_issue_sections(&mut document, result);
- document
-}
-
-fn trade_cancel_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Trade cancel");
- push_trade_identity_fields(&mut document, result);
- common::push_path_field(
- &mut document,
- "Cancel reason",
- result,
- &["cancellation_reason"],
- );
- common::push_bool_field(&mut document, "Dry run", result, &["dry_run"]);
- common::push_path_field(&mut document, "Request", result, &["request_event_id"]);
- common::push_path_field(&mut document, "Decision", result, &["decision_event_id"]);
- common::push_path_field(&mut document, "Event", result, &["event_id"]);
- common::push_path_field(&mut document, "Signer", result, &["signer_mode"]);
- push_relay_field(&mut document, result);
- push_issue_sections(&mut document, result);
- document
-}
-
-fn trade_detail_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Trade");
- push_trade_identity_fields(&mut document, result);
- common::push_bool_field(&mut document, "Submit ready", result, &["ready_for_submit"]);
- common::push_path_field(
- &mut document,
- "Buyer account",
- result,
- &["buyer_account_id"],
- );
- common::push_path_field(&mut document, "Buyer custody", result, &["buyer_custody"]);
- common::push_bool_field(
- &mut document,
- "Buyer can write",
- result,
- &["buyer_write_capable"],
- );
- common::push_path_field(&mut document, "Updated", result, &["updated_at_unix"]);
- push_job_fields(&mut document, result);
- push_workflow_fields(&mut document, result);
- push_trade_items_section(&mut document, result);
- push_issue_sections(&mut document, result);
- document
-}
-
-fn trade_rebind_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Trade rebind");
- common::push_path_field(&mut document, "Lookup", result, &["lookup"]);
- common::push_path_field(&mut document, "From", result, &["from_trade_id"]);
- common::push_path_field(&mut document, "To", result, &["to_trade_id"]);
- common::push_bool_field(
- &mut document,
- "Trade changed",
- result,
- &["trade_id_changed"],
- );
- common::push_path_field(
- &mut document,
- "From account",
- result,
- &["from_buyer_account_id"],
- );
- common::push_path_field(
- &mut document,
- "To account",
- result,
- &["to_buyer_account_id"],
- );
- common::push_bool_field(
- &mut document,
- "Buyer changed",
- result,
- &["buyer_pubkey_changed"],
- );
- common::push_path_field(
- &mut document,
- "Existing request",
- result,
- &["existing_request_check"],
- );
- push_lines_section(
- &mut document,
- "Existing requests",
- result,
- &["existing_request_event_ids"],
- );
- document
-}
-
-fn trade_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Trades");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- let rows = common::array(result, &["trades"])
- .into_iter()
- .flatten()
- .map(|trade| {
- TerminalTableRow::new(vec![
- common::string(trade, &["id"]).unwrap_or_default(),
- common::string(trade, &["state"]).unwrap_or_default(),
- common::bool_path(trade, &["ready_for_submit"])
- .map(ready_blocked)
- .unwrap_or_default()
- .to_owned(),
- common::number_label_path(trade, &["item_count"]).unwrap_or_default(),
- common::string(trade, &["listing_addr"])
- .or_else(|| common::string(trade, &["listing_lookup"]))
- .unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Trades",
- vec![
- TerminalTableColumn::new("Trade", 6, 24),
- TerminalTableColumn::new("State", 5, 14),
- TerminalTableColumn::new("Submit", 6, 8),
- TerminalTableColumn::new("Items", 5, 5),
- TerminalTableColumn::new("Listing", 8, 28),
- ],
- rows,
- "No trade drafts found",
- ));
- document
-}
-
-fn trade_app_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Trade app records");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- common::push_bool_field(&mut document, "More", result, &["has_more"]);
- let rows = common::array(result, &["records"])
- .into_iter()
- .flatten()
- .map(|record| {
- TerminalTableRow::new(vec![
- common::string(record, &["record_id"]).unwrap_or_default(),
- common::string(record, &["status"]).unwrap_or_default(),
- common::string(record, &["trade_id"]).unwrap_or_default(),
- common::bool_path(record, &["exportable"])
- .map(yes_no)
- .unwrap_or_default()
- .to_owned(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Records",
- vec![
- TerminalTableColumn::new("Record", 8, 28),
- TerminalTableColumn::new("Status", 6, 14),
- TerminalTableColumn::new("Trade", 6, 24),
- TerminalTableColumn::new("Export", 6, 6),
- ],
- rows,
- "No app-authored trade records found",
- ));
- document
-}
-
-fn trade_app_export_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Trade app export");
- common::push_path_field(&mut document, "Record", result, &["record_id"]);
- common::push_path_field(&mut document, "Trade", result, &["trade_id"]);
- common::push_bool_field(&mut document, "Dry run", result, &["dry_run"]);
- common::push_bool_field(&mut document, "Valid", result, &["valid"]);
- common::push_path_field(&mut document, "Listing", result, &["listing_addr"]);
- common::push_path_field(
- &mut document,
- "Buyer account",
- result,
- &["buyer_account_id"],
- );
- push_issue_sections(&mut document, result);
- document
-}
-
-fn trade_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Trade status");
- push_trade_identity_fields(&mut document, result);
- common::push_path_field(
- &mut document,
- "Actor context",
- result,
- &["actor_context_source"],
- );
- common::push_path_field(&mut document, "Last event", result, &["last_event_id"]);
- common::push_path_field(&mut document, "Lifecycle", result, &["lifecycle", "phase"]);
- common::push_bool_field(
- &mut document,
- "Terminal",
- result,
- &["lifecycle", "terminal"],
- );
- common::push_path_field(
- &mut document,
- "Validation trust",
- result,
- &["sdk_receipt", "validation_trust", "state"],
- );
- common::push_bool_field(
- &mut document,
- "Production committed",
- result,
- &["sdk_receipt", "validation_trust", "production_committed"],
- );
- common::push_path_field(
- &mut document,
- "Trust reason",
- result,
- &["sdk_receipt", "validation_trust", "reason_code"],
- );
- push_relay_field(&mut document, result);
- push_inventory_section(&mut document, result);
- push_ambiguity_candidates_section(&mut document, result);
- push_issue_sections(&mut document, result);
- push_nested_issues_section(
- &mut document,
- "Lifecycle issues",
- result,
- &["lifecycle", "issues"],
- );
- push_nested_issues_section(&mut document, "Reducer issues", result, &["reducer_issues"]);
- document
-}
-
-fn push_ambiguity_candidates_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["ambiguity_candidates"])
- .into_iter()
- .flatten()
- .map(|candidate| {
- TerminalTableRow::new(vec![
- common::string(candidate, &["status_selector"]).unwrap_or_default(),
- common::string(candidate, &["locator", "root_event_id"]).unwrap_or_default(),
- common::string(candidate, &["status_command"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Ambiguity candidates",
- vec![
- TerminalTableColumn::new("Selector", 8, 32),
- TerminalTableColumn::new("Root", 4, 22),
- TerminalTableColumn::new("Command", 7, 48),
- ],
- rows,
- "No ambiguity candidates",
- ));
-}
-
-fn trade_event_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_title(envelope, "Trade events");
- common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- common::push_path_field(
- &mut document,
- "Actor context",
- result,
- &["actor_context_source"],
- );
- common::push_path_field(&mut document, "Seller", result, &["seller_pubkey"]);
- push_relay_field(&mut document, result);
- let rows = common::array(result, &["trades"])
- .into_iter()
- .flatten()
- .map(|trade| {
- TerminalTableRow::new(vec![
- common::string(trade, &["id"]).unwrap_or_default(),
- common::string(trade, &["state"]).unwrap_or_default(),
- common::string(trade, &["event_id"]).unwrap_or_default(),
- common::number_label_path(trade, &["event_kind"]).unwrap_or_default(),
- common::string(trade, &["listing_addr"])
- .or_else(|| common::string(trade, &["listing_lookup"]))
- .unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Events",
- vec![
- TerminalTableColumn::new("Trade", 6, 22),
- TerminalTableColumn::new("State", 5, 14),
- TerminalTableColumn::new("Event", 5, 22),
- TerminalTableColumn::new("Kind", 4, 6),
- TerminalTableColumn::new("Listing", 8, 24),
- ],
- rows,
- "No trade events found",
- ));
- document
-}
-
-fn trade_event_watch_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Trade event watch");
- common::push_path_field(&mut document, "Trade", result, &["trade_id"]);
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- document
-}
-
-fn push_trade_identity_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(document, "State", result, &["state"]);
- common::push_path_field(document, "Trade", result, &["trade_id"]);
- common::push_path_field(
- document,
- "Locator root",
- result,
- &["locator", "root_event_id"],
- );
- common::push_path_field(
- document,
- "Locator listing",
- result,
- &["locator", "listing_addr"],
- );
- common::push_path_field(
- document,
- "Locator buyer",
- result,
- &["locator", "buyer_pubkey"],
- );
- common::push_path_field(
- document,
- "Locator seller",
- result,
- &["locator", "seller_pubkey"],
- );
- common::push_path_field(document, "Lookup", result, &["lookup"]);
- common::push_path_field(document, "Listing", result, &["listing_addr"]);
- common::push_path_field(document, "Listing", result, &["listing_lookup"]);
- common::push_path_field(document, "Listing event", result, &["listing_event_id"]);
- common::push_path_field(document, "Buyer", result, &["buyer_pubkey"]);
- common::push_path_field(document, "Seller", result, &["seller_pubkey"]);
- common::push_path_field(document, "Reason", result, &["reason"]);
-}
-
-fn push_relay_field(document: &mut TerminalDocument, result: &Value) {
- let acknowledged = array_len(result, &["accepted_transport_endpoints"]);
- let connected = array_len(result, &["attempted_transport_endpoints"]);
- let failed = array_len(result, &["failed_transport_targets"]);
- if acknowledged > 0 || failed > 0 {
- common::push_field(
- document,
- "Targets",
- relay_summary(acknowledged, failed, "acknowledged"),
- );
- } else if connected > 0 || failed > 0 {
- common::push_field(
- document,
- "Targets",
- relay_summary(connected, failed, "connected"),
- );
- }
-}
-
-fn push_job_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(document, "Job", result, &["job", "job_id"]);
- common::push_path_field(document, "Job state", result, &["job", "state"]);
- common::push_path_field(document, "Job event", result, &["job", "event_id"]);
-}
-
-fn push_workflow_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(document, "Workflow", result, &["workflow", "state"]);
- common::push_path_field(
- document,
- "Workflow event",
- result,
- &["workflow", "last_event_id"],
- );
-}
-
-fn push_trade_items_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["items"])
- .into_iter()
- .flatten()
- .map(|item| {
- TerminalTableRow::new(vec![
- common::string(item, &["bin_id"]).unwrap_or_default(),
- common::number_label_path(item, &["bin_count"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Items",
- vec![
- TerminalTableColumn::new("Bin", 3, 18),
- TerminalTableColumn::new("Count", 5, 8),
- ],
- rows,
- "No trade items",
- ));
-}
-
-fn push_inventory_section(document: &mut TerminalDocument, result: &Value) {
- let Some(inventory) = result.get("inventory") else {
- return;
- };
- common::push_path_field(document, "Inventory", inventory, &["state"]);
- common::push_bool_field(document, "Commitment", inventory, &["commitment_valid"]);
- let rows = common::array(inventory, &["bins"])
- .into_iter()
- .flatten()
- .map(|bin| {
- TerminalTableRow::new(vec![
- common::string(bin, &["bin_id"]).unwrap_or_default(),
- common::number_label_path(bin, &["committed_count"]).unwrap_or_default(),
- common::number_label_path(bin, &["available_count"]).unwrap_or_default(),
- common::number_label_path(bin, &["remaining_count"]).unwrap_or_default(),
- common::bool_path(bin, &["over_reserved"])
- .map(yes_no)
- .unwrap_or_default()
- .to_owned(),
- ])
- })
- .collect::<Vec<_>>();
- if !rows.is_empty() {
- document.sections.push(common::table_section(
- "Inventory",
- vec![
- TerminalTableColumn::new("Bin", 3, 16),
- TerminalTableColumn::new("Commit", 6, 8),
- TerminalTableColumn::new("Avail", 5, 8),
- TerminalTableColumn::new("Remain", 6, 8),
- TerminalTableColumn::new("Over", 4, 5),
- ],
- rows,
- "No inventory bins",
- ));
- }
- push_nested_issues_section(document, "Inventory issues", inventory, &["issues"]);
-}
-
-fn push_issue_sections(document: &mut TerminalDocument, result: &Value) {
- push_nested_issues_section(document, "Issues", result, &["issues"]);
-}
-
-fn push_nested_issues_section(
- document: &mut TerminalDocument,
- title: &str,
- value: &Value,
- path: &[&str],
-) {
- let rows = issue_rows(value, path);
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- title,
- vec![
- TerminalTableColumn::new("Code", 8, 32),
- TerminalTableColumn::new("Field", 8, 24),
- TerminalTableColumn::new("Message", 7, 48),
- ],
- rows,
- "No trade issues",
- ));
-}
-
-fn issue_rows(value: &Value, path: &[&str]) -> Vec<TerminalTableRow> {
- common::array(value, path)
- .into_iter()
- .flatten()
- .map(|issue| {
- TerminalTableRow::new(vec![
- common::string(issue, &["code"]).unwrap_or_default(),
- common::string(issue, &["field"]).unwrap_or_default(),
- common::string(issue, &["message"]).unwrap_or_default(),
- ])
- })
- .collect()
-}
-
-fn push_lines_section(document: &mut TerminalDocument, title: &str, value: &Value, path: &[&str]) {
- let lines = common::array(value, path)
- .into_iter()
- .flatten()
- .filter_map(Value::as_str)
- .map(str::to_owned)
- .collect::<Vec<_>>();
- if !lines.is_empty() {
- document
- .sections
- .push(crate::out::terminal::layout::TerminalSection::lines(
- title, lines,
- ));
- }
-}
-
-fn array_len(value: &Value, path: &[&str]) -> usize {
- common::array(value, path).map(Vec::len).unwrap_or_default()
-}
-
-fn trade_decision_title(operation_id: &str) -> &'static str {
- match operation_id {
- "trade.accept" => "Trade accept",
- "trade.decline" => "Trade decline",
- _ => "Trade decision",
- }
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::json;
-
- use crate::out::envelope::{EnvelopeContext, next_actions_from_result_value};
- use crate::out::terminal::renderer::render_terminal_document;
-
- use super::*;
-
- fn success_envelope(operation_id: &str, result: Value) -> OutputEnvelope {
- let mut envelope = OutputEnvelope::success(
- operation_id,
- result,
- EnvelopeContext::new(format!("req_{operation_id}"), false),
- );
- envelope.next_actions = next_actions_from_result_value(&envelope.result);
- envelope
- }
-
- #[test]
- fn trade_list_uses_trade_surface_without_internal_draft_path() {
- let envelope = success_envelope(
- "trade.list",
- json!({
- "state": "ready",
- "count": 1,
- "trades": [{
- "id": "trade_test",
- "state": "ready",
- "ready_for_submit": true,
- "file": "orders/drafts/trade_test.toml",
- "item_count": 1,
- "listing_addr": "listing_test"
- }],
- "actions": ["radroots trade get trade_test"]
- }),
- );
- let document = TRADE_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Trades"));
- assert!(rendered.contains("trade_test"));
- assert!(rendered.contains("radroots trade get trade_test"));
- assert!(!rendered.contains("orders/drafts"));
- assert!(!rendered.contains("Order"));
- }
-
- #[test]
- fn event_watch_result_is_deferred_status_not_stream_claim() {
- let envelope = success_envelope(
- "trade.event.watch",
- json!({
- "state": "not_implemented",
- "trade_id": "trade_test",
- "reason": "Nostr-backed trade event watch is not implemented",
- "actions": ["radroots trade status get trade_test"]
- }),
- );
- let document = TRADE_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Trade event watch"));
- assert!(rendered.contains("not implemented"));
- assert!(rendered.contains("radroots trade status get trade_test"));
- assert!(!rendered.contains("stream"));
- }
-}
diff --git a/src/out/terminal/renderers/v1.rs b/src/out/terminal/renderers/v1.rs
@@ -0,0 +1,25 @@
+use crate::out::envelope::OutputEnvelope;
+use crate::out::terminal::layout::TerminalDocument;
+use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
+use crate::out::terminal::renderer::TerminalRenderContext;
+use crate::registry::OPERATION_REGISTRY;
+
+use super::common;
+
+pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
+ OPERATION_REGISTRY
+ .iter()
+ .fold(registry, |registry, operation| {
+ registry.register(operation.operation_id, &V1_RENDERER)
+ })
+}
+
+struct V1Renderer;
+
+static V1_RENDERER: V1Renderer = V1Renderer;
+
+impl TerminalOperationRenderer for V1Renderer {
+ fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
+ common::base_terminal_document(envelope)
+ }
+}
diff --git a/src/out/terminal/renderers/validation.rs b/src/out/terminal/renderers/validation.rs
@@ -1,361 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
-use crate::out::terminal::values::relay_summary;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("validation.receipt.get", &VALIDATION_RENDERER)
- .register("validation.receipt.list", &VALIDATION_RENDERER)
- .register("validation.receipt.verify", &VALIDATION_RENDERER)
-}
-
-struct ValidationRenderer;
-
-static VALIDATION_RENDERER: ValidationRenderer = ValidationRenderer;
-
-impl TerminalOperationRenderer for ValidationRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::display_source(envelope);
- match envelope.operation_id.as_str() {
- "validation.receipt.list" => validation_receipt_list_document(envelope, result),
- "validation.receipt.verify" => validation_receipt_inspection_document(
- envelope,
- result,
- "Validation receipt verification",
- ),
- _ => validation_receipt_inspection_document(envelope, result, "Validation receipt"),
- }
- }
-}
-
-fn validation_receipt_inspection_document(
- envelope: &OutputEnvelope,
- result: &Value,
- title: &str,
-) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, title);
- common::push_path_field(&mut document, "Receipt", result, &["receipt_event_id"]);
- common::push_path_field(&mut document, "Trade", result, &["order_id"]);
- common::push_path_field(&mut document, "Validation", result, &["validation_state"]);
- push_proof_fields(&mut document, result);
- common::push_path_field(&mut document, "Event", result, &["event", "id"]);
- common::push_path_field(&mut document, "Author", result, &["event", "author"]);
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- push_relay_field(&mut document, result);
- push_receipt_tag_fields(&mut document, result);
- document
-}
-
-fn validation_receipt_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let mut document = common::document_with_status_title(envelope, "Validation receipts");
- common::push_path_field(&mut document, "Trade", result, &["order_id"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
- common::push_count_field(&mut document, "Valid", result, &["valid_count"]);
- common::push_count_field(&mut document, "Invalid", result, &["invalid_count"]);
- common::push_path_field(&mut document, "Reason", result, &["reason"]);
- push_relay_field(&mut document, result);
- let rows = common::array(result, &["receipts"])
- .into_iter()
- .flatten()
- .map(|receipt| {
- TerminalTableRow::new(vec![
- common::string(receipt, &["receipt_event_id"]).unwrap_or_default(),
- common::string(receipt, &["result"]).unwrap_or_default(),
- proof_summary_from_summary(receipt),
- common::string(receipt, &["receipt_type"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- document.sections.push(common::table_section(
- "Receipts",
- vec![
- TerminalTableColumn::new("Receipt", 7, 24),
- TerminalTableColumn::new("Result", 6, 12),
- TerminalTableColumn::new("Proof", 5, 18),
- TerminalTableColumn::new("Type", 4, 18),
- ],
- rows,
- "No validation receipts found",
- ));
- push_invalid_receipts_section(&mut document, result);
- document
-}
-
-fn push_proof_fields(document: &mut TerminalDocument, result: &Value) {
- let Some(proof) = result.get("proof_verification") else {
- return;
- };
- common::push_field(document, "Proof", proof_state_label(proof));
- common::push_path_field(document, "Proof system", proof, &["proof_system"]);
- common::push_path_field(document, "Authority", proof, &["validation_authority"]);
- common::push_path_field(document, "Confidence", proof, &["commitment_confidence"]);
- common::push_bool_field(
- document,
- "Cryptographic proof",
- proof,
- &["cryptographic_proof_verified"],
- );
- common::push_path_field(document, "Verifier", proof, &["verifier"]);
- common::push_path_field(document, "Proof mode", proof, &["mode"]);
- common::push_path_field(document, "Program", proof, &["program_hash"]);
- common::push_path_field(document, "Verifying key", proof, &["verifying_key_hash"]);
- common::push_path_field(document, "Proof reason", proof, &["reason"]);
-}
-
-fn push_receipt_tag_fields(document: &mut TerminalDocument, result: &Value) {
- common::push_path_field(
- document,
- "Receipt type",
- result,
- &["receipt_tags", "receipt_type"],
- );
- common::push_path_field(
- document,
- "Root event",
- result,
- &["receipt_tags", "root_event_id"],
- );
- common::push_path_field(
- document,
- "Target event",
- result,
- &["receipt_tags", "target_event_id"],
- );
-}
-
-fn push_invalid_receipts_section(document: &mut TerminalDocument, result: &Value) {
- let rows = common::array(result, &["invalid_receipts"])
- .into_iter()
- .flatten()
- .map(|receipt| {
- TerminalTableRow::new(vec![
- common::string(receipt, &["receipt_event_id"]).unwrap_or_default(),
- common::string(receipt, &["reason_code"]).unwrap_or_default(),
- common::string(receipt, &["reason"]).unwrap_or_default(),
- ])
- })
- .collect::<Vec<_>>();
- if rows.is_empty() {
- return;
- }
- document.sections.push(common::table_section(
- "Invalid receipts",
- vec![
- TerminalTableColumn::new("Receipt", 7, 24),
- TerminalTableColumn::new("Code", 8, 28),
- TerminalTableColumn::new("Reason", 6, 46),
- ],
- rows,
- "No invalid validation receipts",
- ));
-}
-
-fn push_relay_field(document: &mut TerminalDocument, result: &Value) {
- let connected = array_len(result, &["attempted_transport_endpoints"]);
- let failed = array_len(result, &["failed_transport_targets"]);
- if connected > 0 || failed > 0 {
- common::push_field(
- document,
- "Targets",
- relay_summary(connected, failed, "connected"),
- );
- }
-}
-
-fn proof_state_label(proof: &Value) -> String {
- match common::string(proof, &["state"]).as_deref() {
- Some("validator_set_deterministic_receipt") => {
- return "validator-set deterministic".to_owned();
- }
- Some("pending_validator_validation") => return "pending validator".to_owned(),
- Some("validator_set_validated") => return "validator set".to_owned(),
- Some("validator_set_and_proof_verified") => return "validator set + proof".to_owned(),
- Some("sp1_inline_proof_verified") => return "cryptographic proof".to_owned(),
- Some("cryptographic_proof_verified") => return "cryptographic proof".to_owned(),
- Some("sp1_execute_checked") => return "SP1 execute checked".to_owned(),
- Some("validator_evidence_trust_metadata_missing") => {
- return "trust metadata missing".to_owned();
- }
- Some("validator_evidence_trust_metadata_mismatch") => {
- return "trust metadata mismatch".to_owned();
- }
- Some("validation_receipt_validator_evidence_invalid") => {
- return "validator evidence invalid".to_owned();
- }
- _ => {}
- }
- if common::bool_path(proof, &["cryptographic_proof_verified"]) == Some(true) {
- return "verified".to_owned();
- }
- if common::bool_path(proof, &["cryptographic_proof_required"]) == Some(true) {
- return "unverified".to_owned();
- }
- "not required".to_owned()
-}
-
-fn proof_summary_from_summary(receipt: &Value) -> String {
- match common::string(receipt, &["proof_verification_state"]).as_deref() {
- Some("validator_set_deterministic_receipt") => "validator-set".to_owned(),
- Some("pending_validator_validation") => "pending validator".to_owned(),
- Some("validator_set_validated") => "validator set".to_owned(),
- Some("validator_set_and_proof_verified") => "validator+proof".to_owned(),
- Some("sp1_inline_proof_verified") => "proof verified".to_owned(),
- Some("cryptographic_proof_verified") => "proof verified".to_owned(),
- Some("sp1_execute_checked") => "execute checked".to_owned(),
- Some("validator_evidence_trust_metadata_missing") => "trust missing".to_owned(),
- Some("validator_evidence_trust_metadata_mismatch") => "trust mismatch".to_owned(),
- Some("validation_receipt_validator_evidence_invalid") => "invalid".to_owned(),
- Some(_) => "available".to_owned(),
- None => String::new(),
- }
-}
-
-fn array_len(value: &Value, path: &[&str]) -> usize {
- common::array(value, path).map(Vec::len).unwrap_or_default()
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::json;
-
- use crate::out::envelope::{EnvelopeContext, next_actions_from_result_value};
- use crate::out::terminal::renderer::render_terminal_document;
-
- use super::*;
-
- fn success_envelope(operation_id: &str, result: Value) -> OutputEnvelope {
- let mut envelope = OutputEnvelope::success(
- operation_id,
- result,
- EnvelopeContext::new(format!("req_{operation_id}"), false),
- );
- envelope.next_actions = next_actions_from_result_value(&envelope.result);
- envelope
- }
-
- #[test]
- fn receipt_list_renders_trade_id_surface() {
- let envelope = success_envelope(
- "validation.receipt.list",
- json!({
- "state": "empty",
- "order_id": "trade_test",
- "count": 0,
- "valid_count": 0,
- "invalid_count": 0,
- "receipts": [],
- "invalid_receipts": [],
- "actions": ["radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"]
- }),
- );
- let document = VALIDATION_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Validation receipts"));
- assert!(rendered.contains("trade_test"));
- assert!(!rendered.contains("--order-id"));
- }
-
- #[test]
- fn proof_output_does_not_claim_verified_without_crypto_source() {
- let envelope = success_envelope(
- "validation.receipt.verify",
- json!({
- "state": "valid",
- "receipt_event_id": "receipt_test",
- "order_id": "trade_test",
- "validation_state": "valid",
- "proof_verification": {
- "state": "sp1_reference_unresolved",
- "proof_system": "sp1_core",
- "cryptographic_proof_required": true,
- "cryptographic_proof_verified": false,
- "verifier": "radroots_cli_validation_receipt_v1"
- },
- "actions": []
- }),
- );
- let document = VALIDATION_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Proof unverified"));
- assert!(!rendered.contains("Proof verified"));
- }
-
- #[test]
- fn proof_output_renders_local_only_trust_state() {
- let envelope = success_envelope(
- "validation.receipt.verify",
- json!({
- "state": "invalid",
- "receipt_event_id": "receipt_test",
- "order_id": "trade_test",
- "validation_state": "invalid",
- "proof_verification": {
- "state": "validator_set_deterministic_receipt",
- "proof_system": "none",
- "validation_authority": "validator_set_deterministic",
- "commitment_confidence": "committed_by_validator_set",
- "production_verification": false,
- "cryptographic_proof_required": false,
- "cryptographic_proof_verified": false,
- "verifier": "radroots_cli_validation_receipt_v1"
- },
- "actions": []
- }),
- );
- let document = VALIDATION_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("Proof validator-set deterministic"));
- assert!(rendered.contains("Authority validator_set_deterministic"));
- assert!(rendered.contains("Confidence local_only"));
- assert!(!rendered.contains("Proof verified"));
- }
-
- #[test]
- fn receipt_list_summarizes_trust_state() {
- let envelope = success_envelope(
- "validation.receipt.list",
- json!({
- "state": "listed",
- "order_id": "trade_test",
- "count": 2,
- "valid_count": 2,
- "invalid_count": 0,
- "receipts": [
- {
- "receipt_event_id": "receipt_local",
- "result": "valid",
- "proof_verification_state": "validator_set_deterministic_receipt",
- "receipt_type": "trade_transition"
- },
- {
- "receipt_event_id": "receipt_trusted",
- "result": "valid",
- "proof_verification_state": "validator_set_validated",
- "receipt_type": "trade_transition"
- }
- ],
- "invalid_receipts": [],
- "actions": []
- }),
- );
- let document = VALIDATION_RENDERER.render(&envelope, &TerminalRenderContext::default());
- let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
-
- assert!(rendered.contains("validator-set"));
- assert!(rendered.contains("validator set"));
- assert!(!rendered.contains("not required"));
- }
-}
diff --git a/src/out/terminal/renderers/workspace.rs b/src/out/terminal/renderers/workspace.rs
@@ -1,37 +0,0 @@
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- registry
- .register("workspace.init", &WORKSPACE_RENDERER)
- .register("workspace.get", &WORKSPACE_RENDERER)
-}
-
-struct WorkspaceRenderer;
-
-static WORKSPACE_RENDERER: WorkspaceRenderer = WorkspaceRenderer;
-
-impl TerminalOperationRenderer for WorkspaceRenderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- if !envelope.errors.is_empty() {
- return common::base_terminal_document(envelope);
- }
- let result = common::result(envelope);
- let mut document = common::document_with_title(
- envelope,
- match envelope.operation_id.as_str() {
- "workspace.init" => "Workspace initialized",
- _ => "Workspace ready",
- },
- );
- common::push_path_field(&mut document, "Profile", result, &["profile"]);
- common::push_verbose_path_field(&mut document, "Config", result, &["app_config_path"]);
- common::push_verbose_path_field(&mut document, "Data", result, &["app_data_root"]);
- common::push_verbose_path_field(&mut document, "Replica", result, &["replica_store_path"]);
- document
- }
-}
diff --git a/src/registry/account.rs b/src/registry/account.rs
@@ -1,145 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const ACCOUNT_CREATE: OperationSpec = operation!(
- "account.create",
- "radroots account create",
- "account",
- "account_create",
- "AccountCreateRequest",
- "AccountCreateResult",
- "Create a local account identity.",
- Any,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const ACCOUNT_IMPORT: OperationSpec = operation!(
- "account.import",
- "radroots account import",
- "account",
- "account_import",
- "AccountImportRequest",
- "AccountImportResult",
- "Import an existing account identity.",
- Any,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const ACCOUNT_ATTACH_SECRET: OperationSpec = operation!(
- "account.attach_secret",
- "radroots account attach-secret",
- "account",
- "account_attach_secret",
- "AccountAttachSecretRequest",
- "AccountAttachSecretResult",
- "Attach local secret custody to an existing account.",
- Any,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const ACCOUNT_GET: OperationSpec = operation!(
- "account.get",
- "radroots account get",
- "account",
- "account_get",
- "AccountGetRequest",
- "AccountGetResult",
- "Get account details.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const ACCOUNT_LIST: OperationSpec = operation!(
- "account.list",
- "radroots account list",
- "account",
- "account_list",
- "AccountListRequest",
- "AccountListResult",
- "List known local accounts.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const ACCOUNT_REMOVE: OperationSpec = operation!(
- "account.remove",
- "radroots account remove",
- "account",
- "account_remove",
- "AccountRemoveRequest",
- "AccountRemoveResult",
- "Remove an account from local configuration/store.",
- Any,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const ACCOUNT_SELECTION_GET: OperationSpec = operation!(
- "account.selection.get",
- "radroots account selection get",
- "account",
- "account_selection_get",
- "AccountSelectionGetRequest",
- "AccountSelectionGetResult",
- "Get selected account context.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const ACCOUNT_SELECTION_UPDATE: OperationSpec = operation!(
- "account.selection.update",
- "radroots account selection update",
- "account",
- "account_selection_update",
- "AccountSelectionUpdateRequest",
- "AccountSelectionUpdateResult",
- "Update selected account context.",
- Any,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const ACCOUNT_SELECTION_CLEAR: OperationSpec = operation!(
- "account.selection.clear",
- "radroots account selection clear",
- "account",
- "account_selection_clear",
- "AccountSelectionClearRequest",
- "AccountSelectionClearResult",
- "Clear selected account context.",
- Any,
- true,
- None,
- Medium,
- false,
- true
-);
diff --git a/src/registry/basket.rs b/src/registry/basket.rs
@@ -1,161 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const BASKET_CREATE: OperationSpec = operation!(
- "basket.create",
- "radroots basket create",
- "basket",
- "basket_create",
- "BasketCreateRequest",
- "BasketCreateResult",
- "Create local basket.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const BASKET_GET: OperationSpec = operation!(
- "basket.get",
- "radroots basket get",
- "basket",
- "basket_get",
- "BasketGetRequest",
- "BasketGetResult",
- "Get local basket.",
- Buyer,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const BASKET_LIST: OperationSpec = operation!(
- "basket.list",
- "radroots basket list",
- "basket",
- "basket_list",
- "BasketListRequest",
- "BasketListResult",
- "List local baskets.",
- Buyer,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const BASKET_ITEM_ADD: OperationSpec = operation!(
- "basket.item.add",
- "radroots basket item add",
- "basket",
- "basket_item_add",
- "BasketItemAddRequest",
- "BasketItemAddResult",
- "Add item to local basket.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const BASKET_ITEM_UPDATE: OperationSpec = operation!(
- "basket.item.update",
- "radroots basket item update",
- "basket",
- "basket_item_update",
- "BasketItemUpdateRequest",
- "BasketItemUpdateResult",
- "Update local basket item.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const BASKET_ITEM_REMOVE: OperationSpec = operation!(
- "basket.item.remove",
- "radroots basket item remove",
- "basket",
- "basket_item_remove",
- "BasketItemRemoveRequest",
- "BasketItemRemoveResult",
- "Remove item from local basket.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const BASKET_ADJUSTMENT_ADD: OperationSpec = operation!(
- "basket.adjustment.add",
- "radroots basket adjustment add",
- "basket",
- "basket_adjustment_add",
- "BasketAdjustmentAddRequest",
- "BasketAdjustmentAddResult",
- "Add buyer basket adjustment.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const BASKET_ADJUSTMENT_REMOVE: OperationSpec = operation!(
- "basket.adjustment.remove",
- "radroots basket adjustment remove",
- "basket",
- "basket_adjustment_remove",
- "BasketAdjustmentRemoveRequest",
- "BasketAdjustmentRemoveResult",
- "Remove buyer basket adjustment.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const BASKET_VALIDATE: OperationSpec = operation!(
- "basket.validate",
- "radroots basket validate",
- "basket",
- "basket_validate",
- "BasketValidateRequest",
- "BasketValidateResult",
- "Validate basket orderability.",
- Buyer,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const BASKET_QUOTE_CREATE: OperationSpec = operation!(
- "basket.quote.create",
- "radroots basket quote create",
- "basket",
- "basket_quote_create",
- "BasketQuoteCreateRequest",
- "BasketQuoteCreateResult",
- "Create deterministic basket quote.",
- Buyer,
- true,
- Conditional,
- Medium,
- false,
- true
-);
diff --git a/src/registry/config.rs b/src/registry/config.rs
@@ -1,17 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const CONFIG_GET: OperationSpec = operation!(
- "config.get",
- "radroots config get",
- "config",
- "config_get",
- "ConfigGetRequest",
- "ConfigGetResult",
- "Read effective configuration.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
diff --git a/src/registry/farm.rs b/src/registry/farm.rs
@@ -1,161 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const FARM_CREATE: OperationSpec = operation!(
- "farm.create",
- "radroots farm create",
- "farm",
- "farm_create",
- "FarmCreateRequest",
- "FarmCreateResult",
- "Create farm identity/profile resource.",
- Seller,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const FARM_GET: OperationSpec = operation!(
- "farm.get",
- "radroots farm get",
- "farm",
- "farm_get",
- "FarmGetRequest",
- "FarmGetResult",
- "Get farm resource.",
- Seller,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const FARM_REBIND: OperationSpec = operation!(
- "farm.rebind",
- "radroots farm rebind",
- "farm",
- "farm_rebind",
- "FarmRebindRequest",
- "FarmRebindResult",
- "Rebind farm seller account.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const FARM_PROFILE_UPDATE: OperationSpec = operation!(
- "farm.profile.update",
- "radroots farm profile update",
- "farm",
- "farm_profile_update",
- "FarmProfileUpdateRequest",
- "FarmProfileUpdateResult",
- "Update farm public profile fields.",
- Seller,
- true,
- Conditional,
- Medium,
- false,
- true
-);
-
-pub const FARM_LOCATION_SET: OperationSpec = operation!(
- "farm.location.set",
- "radroots farm location set --lat 48.429456 --lng -123.349786",
- "farm",
- "farm_location_set",
- "FarmLocationSetRequest",
- "FarmLocationSetResult",
- "Set private farm location and derived public locality.",
- Seller,
- true,
- Conditional,
- Medium,
- true,
- true
-);
-
-pub const FARM_LOCATION_GET: OperationSpec = operation!(
- "farm.location.get",
- "radroots farm location get",
- "farm",
- "farm_location_get",
- "FarmLocationGetRequest",
- "FarmLocationGetResult",
- "Get private exact farm location for the configured farm.",
- Seller,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const FARM_LOCATION_CLEAR: OperationSpec = operation!(
- "farm.location.clear",
- "radroots farm location clear",
- "farm",
- "farm_location_clear",
- "FarmLocationClearRequest",
- "FarmLocationClearResult",
- "Clear private exact farm location for the configured farm.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const FARM_FULFILLMENT_UPDATE: OperationSpec = operation!(
- "farm.fulfillment.update",
- "radroots farm fulfillment update",
- "farm",
- "farm_fulfillment_update",
- "FarmFulfillmentUpdateRequest",
- "FarmFulfillmentUpdateResult",
- "Update farm fulfillment posture.",
- Seller,
- true,
- Conditional,
- Medium,
- false,
- true
-);
-
-pub const FARM_READINESS_CHECK: OperationSpec = operation!(
- "farm.readiness.check",
- "radroots farm readiness check",
- "farm",
- "farm_readiness_check",
- "FarmReadinessCheckRequest",
- "FarmReadinessCheckResult",
- "Check whether farm is publish-ready.",
- Seller,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const FARM_PUBLISH: OperationSpec = operation!(
- "farm.publish",
- "radroots farm publish",
- "farm",
- "farm_publish",
- "FarmPublishRequest",
- "FarmPublishResult",
- "Publish farm identity/profile externally.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
diff --git a/src/registry/health.rs b/src/registry/health.rs
@@ -1,33 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const HEALTH_STATUS_GET: OperationSpec = operation!(
- "health.status.get",
- "radroots health status get",
- "health",
- "health_status_get",
- "HealthStatusGetRequest",
- "HealthStatusGetResult",
- "Get concise health and readiness status.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const HEALTH_CHECK_RUN: OperationSpec = operation!(
- "health.check.run",
- "radroots health check run",
- "health",
- "health_check_run",
- "HealthCheckRunRequest",
- "HealthCheckRunResult",
- "Run comprehensive diagnostics.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
diff --git a/src/registry/listing.rs b/src/registry/listing.rs
@@ -1,161 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const LISTING_CREATE: OperationSpec = operation!(
- "listing.create",
- "radroots listing create",
- "listing",
- "listing_create",
- "ListingCreateRequest",
- "ListingCreateResult",
- "Create seller-owned listing/offer.",
- Seller,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const LISTING_GET: OperationSpec = operation!(
- "listing.get",
- "radroots listing get",
- "listing",
- "listing_get",
- "ListingGetRequest",
- "ListingGetResult",
- "Get seller-owned listing/offer.",
- Seller,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const LISTING_LIST: OperationSpec = operation!(
- "listing.list",
- "radroots listing list",
- "listing",
- "listing_list",
- "ListingListRequest",
- "ListingListResult",
- "List seller-owned listings/offers.",
- Seller,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const LISTING_APP_LIST: OperationSpec = operation!(
- "listing.app.list",
- "radroots listing app list",
- "listing",
- "listing_app_list",
- "ListingAppListRequest",
- "ListingAppListResult",
- "List app-authored shared local listing records.",
- Seller,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const LISTING_APP_EXPORT: OperationSpec = operation!(
- "listing.app.export",
- "radroots listing app export",
- "listing",
- "listing_app_export",
- "ListingAppExportRequest",
- "ListingAppExportResult",
- "Export an app-authored shared listing record as a CLI draft.",
- Seller,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const LISTING_UPDATE: OperationSpec = operation!(
- "listing.update",
- "radroots listing update",
- "listing",
- "listing_update",
- "ListingUpdateRequest",
- "ListingUpdateResult",
- "Update general listing fields.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const LISTING_VALIDATE: OperationSpec = operation!(
- "listing.validate",
- "radroots listing validate",
- "listing",
- "listing_validate",
- "ListingValidateRequest",
- "ListingValidateResult",
- "Validate listing for publication or orderability.",
- Seller,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const LISTING_REBIND: OperationSpec = operation!(
- "listing.rebind",
- "radroots listing rebind",
- "listing",
- "listing_rebind",
- "ListingRebindRequest",
- "ListingRebindResult",
- "Rebind a listing draft to an explicit seller actor.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const LISTING_PUBLISH: OperationSpec = operation!(
- "listing.publish",
- "radroots listing publish",
- "listing",
- "listing_publish",
- "ListingPublishRequest",
- "ListingPublishResult",
- "Publish listing externally.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const LISTING_ARCHIVE: OperationSpec = operation!(
- "listing.archive",
- "radroots listing archive",
- "listing",
- "listing_archive",
- "ListingArchiveRequest",
- "ListingArchiveResult",
- "Archive listing resource.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
diff --git a/src/registry/market.rs b/src/registry/market.rs
@@ -1,49 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const MARKET_REFRESH: OperationSpec = operation!(
- "market.refresh",
- "radroots market refresh",
- "market",
- "market_refresh",
- "MarketRefreshRequest",
- "MarketRefreshResult",
- "Refresh local market projection.",
- Buyer,
- true,
- None,
- Medium,
- true,
- true
-);
-
-pub const MARKET_PRODUCT_SEARCH: OperationSpec = operation!(
- "market.product.search",
- "radroots market product search",
- "market",
- "market_product_search",
- "MarketProductSearchRequest",
- "MarketProductSearchResult",
- "Search market products/listings.",
- Buyer,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const MARKET_LISTING_GET: OperationSpec = operation!(
- "market.listing.get",
- "radroots market listing get",
- "market",
- "market_listing_get",
- "MarketListingGetRequest",
- "MarketListingGetResult",
- "Get market listing details.",
- Buyer,
- false,
- None,
- Low,
- false,
- false
-);
diff --git a/src/registry/mesh.rs b/src/registry/mesh.rs
@@ -1,65 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const MESH_SCOPE_GET: OperationSpec = operation!(
- "mesh.scope.get",
- "radroots mesh scope get",
- "mesh",
- "mesh_scope_get",
- "MeshScopeGetRequest",
- "MeshScopeGetResult",
- "Read the configured mesh scope.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const MESH_SCOPE_SET: OperationSpec = operation!(
- "mesh.scope.set",
- "radroots mesh scope set --scope disabled",
- "mesh",
- "mesh_scope_set",
- "MeshScopeSetRequest",
- "MeshScopeSetResult",
- "Write the configured mesh scope.",
- Any,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const MESH_STATUS: OperationSpec = operation!(
- "mesh.status",
- "radroots mesh status",
- "mesh",
- "mesh_status",
- "MeshStatusRequest",
- "MeshStatusResult",
- "Read mesh implementation status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const MESH_POLICY_CHECK: OperationSpec = operation!(
- "mesh.policy.check",
- "radroots mesh policy check",
- "mesh",
- "mesh_policy_check",
- "MeshPolicyCheckRequest",
- "MeshPolicyCheckResult",
- "Evaluate mesh delivery policy for the active Reticulum availability state.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
diff --git a/src/registry/mod.rs b/src/registry/mod.rs
@@ -1,5 +1,13 @@
+use radroots_runtime_contract_v1::{
+ ApprovalRequirementV1, DryRunSupportV1, IdempotencyPolicyV1, OperationMutabilityV1,
+ RuntimeOperationDescriptorV1, RuntimeOperationIdV1, SignerRequirementV1,
+ TransportCapabilityRouteV1,
+};
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OperationSpec {
+ pub runtime_operation_id: RuntimeOperationIdV1,
+ pub descriptor: RuntimeOperationDescriptorV1,
pub operation_id: &'static str,
pub cli_path: &'static str,
pub namespace: &'static str,
@@ -9,27 +17,33 @@ pub struct OperationSpec {
pub json_kind: &'static str,
pub description: &'static str,
pub role: OperationRole,
- pub mutates: bool,
- pub approval_policy: ApprovalPolicy,
- pub risk_level: RiskLevel,
pub supports_json: bool,
pub supports_ndjson: bool,
- pub supports_dry_run: bool,
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ApprovalPolicy {
- None,
- Conditional,
- Required,
-}
+impl OperationSpec {
+ pub fn mutates(self) -> bool {
+ self.descriptor.mutability == OperationMutabilityV1::Mutation
+ }
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RiskLevel {
- Low,
- Medium,
- High,
- Critical,
+ pub fn supports_dry_run(self) -> bool {
+ self.descriptor.dry_run == DryRunSupportV1::PureLocalPlan
+ }
+
+ pub fn requires_idempotency(self) -> bool {
+ self.descriptor.idempotency == IdempotencyPolicyV1::RequiredUuidV7
+ }
+
+ pub fn forbids_idempotency(self) -> bool {
+ self.descriptor.idempotency == IdempotencyPolicyV1::Forbidden
+ }
+
+ pub fn requires_approval(self) -> bool {
+ matches!(
+ self.descriptor.approval,
+ ApprovalRequirementV1::Required | ApprovalRequirementV1::ConditionalOrRequiredByMode
+ )
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -45,141 +59,7 @@ pub enum NetworkRequirement {
External { dry_run_requires_network: bool },
}
-macro_rules! operation {
- (
- $operation_id:literal,
- $cli_path:literal,
- $namespace:literal,
- $mcp_tool:literal,
- $rust_request:literal,
- $rust_result:literal,
- $description:literal,
- $role:ident,
- $mutates:literal,
- $approval_policy:ident,
- $risk_level:ident,
- $supports_ndjson:literal,
- $supports_dry_run:literal
- ) => {
- OperationSpec {
- operation_id: $operation_id,
- cli_path: $cli_path,
- namespace: $namespace,
- mcp_tool: $mcp_tool,
- rust_request: $rust_request,
- rust_result: $rust_result,
- json_kind: $operation_id,
- description: $description,
- role: OperationRole::$role,
- mutates: $mutates,
- approval_policy: ApprovalPolicy::$approval_policy,
- risk_level: RiskLevel::$risk_level,
- supports_json: true,
- supports_ndjson: $supports_ndjson,
- supports_dry_run: $supports_dry_run,
- }
- };
-}
-
-mod account;
-mod basket;
-mod config;
-mod farm;
-mod health;
-mod listing;
-mod market;
-mod mesh;
-mod signer;
-mod store;
-mod sync;
-mod trade;
-mod transport;
-mod validation;
-mod workspace;
-
-pub const OPERATION_REGISTRY: &[OperationSpec] = &[
- workspace::WORKSPACE_INIT,
- workspace::WORKSPACE_GET,
- health::HEALTH_STATUS_GET,
- health::HEALTH_CHECK_RUN,
- config::CONFIG_GET,
- account::ACCOUNT_CREATE,
- account::ACCOUNT_IMPORT,
- account::ACCOUNT_ATTACH_SECRET,
- account::ACCOUNT_GET,
- account::ACCOUNT_LIST,
- account::ACCOUNT_REMOVE,
- account::ACCOUNT_SELECTION_GET,
- account::ACCOUNT_SELECTION_UPDATE,
- account::ACCOUNT_SELECTION_CLEAR,
- signer::SIGNER_STATUS_GET,
- transport::TRANSPORT_PROFILE_GET,
- transport::TRANSPORT_PROFILE_SET,
- transport::TRANSPORT_STATUS,
- transport::TRANSPORT_OUTBOX_STATUS,
- transport::TRANSPORT_OUTBOX_PUSH,
- mesh::MESH_SCOPE_GET,
- mesh::MESH_SCOPE_SET,
- mesh::MESH_STATUS,
- mesh::MESH_POLICY_CHECK,
- store::STORE_INIT,
- store::STORE_STATUS_GET,
- store::STORE_EXPORT,
- store::STORE_BACKUP_CREATE,
- store::STORE_BACKUP_RESTORE,
- sync::SYNC_STATUS_GET,
- sync::SYNC_PULL,
- sync::SYNC_PUSH,
- sync::SYNC_WATCH,
- farm::FARM_CREATE,
- farm::FARM_GET,
- farm::FARM_REBIND,
- farm::FARM_PROFILE_UPDATE,
- farm::FARM_LOCATION_SET,
- farm::FARM_LOCATION_GET,
- farm::FARM_LOCATION_CLEAR,
- farm::FARM_FULFILLMENT_UPDATE,
- farm::FARM_READINESS_CHECK,
- farm::FARM_PUBLISH,
- listing::LISTING_CREATE,
- listing::LISTING_GET,
- listing::LISTING_LIST,
- listing::LISTING_APP_LIST,
- listing::LISTING_APP_EXPORT,
- listing::LISTING_UPDATE,
- listing::LISTING_VALIDATE,
- listing::LISTING_REBIND,
- listing::LISTING_PUBLISH,
- listing::LISTING_ARCHIVE,
- market::MARKET_REFRESH,
- market::MARKET_PRODUCT_SEARCH,
- market::MARKET_LISTING_GET,
- basket::BASKET_CREATE,
- basket::BASKET_GET,
- basket::BASKET_LIST,
- basket::BASKET_ITEM_ADD,
- basket::BASKET_ITEM_UPDATE,
- basket::BASKET_ITEM_REMOVE,
- basket::BASKET_ADJUSTMENT_ADD,
- basket::BASKET_ADJUSTMENT_REMOVE,
- basket::BASKET_VALIDATE,
- basket::BASKET_QUOTE_CREATE,
- trade::TRADE_SUBMIT,
- trade::TRADE_GET,
- trade::TRADE_LIST,
- trade::TRADE_APP_LIST,
- trade::TRADE_APP_EXPORT,
- trade::TRADE_REBIND,
- trade::TRADE_ACCEPT,
- trade::TRADE_DECLINE,
- trade::TRADE_CANCEL,
- trade::TRADE_STATUS_GET,
- trade::TRADE_EVENT_LIST,
- trade::TRADE_EVENT_WATCH,
- validation::VALIDATION_RECEIPT_GET,
- validation::VALIDATION_RECEIPT_LIST,
- validation::VALIDATION_RECEIPT_VERIFY,
-];
+include!("../generated/runtime_contract_registry.rs");
pub fn get_operation(operation_id: &str) -> Option<&'static OperationSpec> {
OPERATION_REGISTRY
@@ -187,52 +67,35 @@ pub fn get_operation(operation_id: &str) -> Option<&'static OperationSpec> {
.find(|operation| operation.operation_id == operation_id)
}
+pub fn runtime_operation_id(operation_id: &str) -> Option<RuntimeOperationIdV1> {
+ RuntimeOperationIdV1::parse(operation_id).ok()
+}
+
pub fn network_requirement(operation_id: &str) -> NetworkRequirement {
- match operation_id {
- "sync.pull"
- | "sync.push"
- | "transport.outbox.push"
- | "sync.watch"
- | "market.refresh"
- | "farm.publish"
- | "listing.publish"
- | "listing.update"
- | "listing.archive"
- | "trade.submit"
- | "trade.event.list"
- | "validation.receipt.get"
- | "validation.receipt.list"
- | "validation.receipt.verify" => NetworkRequirement::External {
- dry_run_requires_network: false,
- },
- "trade.accept" | "trade.decline" | "trade.cancel" => NetworkRequirement::External {
- dry_run_requires_network: true,
- },
- _ => NetworkRequirement::Local,
+ let Some(spec) = get_operation(operation_id) else {
+ return NetworkRequirement::Local;
+ };
+ if requires_external_transport(spec.descriptor.transport_capability) {
+ NetworkRequirement::External {
+ dry_run_requires_network: spec.descriptor.dry_run != DryRunSupportV1::PureLocalPlan,
+ }
+ } else {
+ NetworkRequirement::Local
}
}
pub fn requires_local_signer_mode(operation_id: &str) -> bool {
- matches!(
- operation_id,
- "sync.push"
- | "transport.outbox.push"
- | "trade.submit"
- | "trade.accept"
- | "trade.decline"
- | "trade.cancel"
- )
+ get_operation(operation_id).is_some_and(|operation| {
+ matches!(
+ operation.descriptor.signer,
+ SignerRequirementV1::Required | SignerRequirementV1::ConditionalRelayAuth
+ )
+ })
}
pub fn requires_delivery_capable_transport_profile(operation_id: &str) -> bool {
- matches!(
- operation_id,
- "transport.outbox.push"
- | "farm.publish"
- | "listing.publish"
- | "listing.update"
- | "listing.archive"
- )
+ get_operation(operation_id)
+ .is_some_and(|operation| operation.descriptor.transport_capability.deliver)
}
pub fn registry_linkage_is_valid() -> bool {
@@ -241,419 +104,88 @@ pub fn registry_linkage_is_valid() -> bool {
&& operation.operation_id == operation.json_kind
&& operation.mcp_tool == operation.operation_id.replace('.', "_")
&& operation.supports_json
+ && operation.descriptor.operation_id == operation.runtime_operation_id
+ && operation.descriptor.operation_id.as_str() == operation.operation_id
})
}
+fn requires_external_transport(capability: TransportCapabilityRouteV1) -> bool {
+ capability.deliver || capability.fetch || capability.synchronize || capability.diagnostics
+}
+
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
- use super::{
- ApprovalPolicy, NetworkRequirement, OPERATION_REGISTRY, OperationRole, RiskLevel,
- get_operation, network_requirement, requires_delivery_capable_transport_profile,
- requires_local_signer_mode,
- };
+ use radroots_runtime_contract_v1::{OperationRiskV1, RUNTIME_OPERATION_DESCRIPTORS_V1};
- const EXPECTED_OPERATION_IDS: &[&str] = &[
- "workspace.init",
- "workspace.get",
- "health.status.get",
- "health.check.run",
- "config.get",
- "account.create",
- "account.import",
- "account.attach_secret",
- "account.get",
- "account.list",
- "account.remove",
- "account.selection.get",
- "account.selection.update",
- "account.selection.clear",
- "signer.status.get",
- "transport.profile.get",
- "transport.profile.set",
- "transport.status",
- "transport.outbox.status",
- "transport.outbox.push",
- "mesh.scope.get",
- "mesh.scope.set",
- "mesh.status",
- "mesh.policy.check",
- "store.init",
- "store.status.get",
- "store.export",
- "store.backup.create",
- "store.backup.restore",
- "sync.status.get",
- "sync.pull",
- "sync.push",
- "sync.watch",
- "farm.create",
- "farm.get",
- "farm.rebind",
- "farm.profile.update",
- "farm.location.set",
- "farm.location.get",
- "farm.location.clear",
- "farm.fulfillment.update",
- "farm.readiness.check",
- "farm.publish",
- "listing.create",
- "listing.get",
- "listing.list",
- "listing.app.list",
- "listing.app.export",
- "listing.update",
- "listing.validate",
- "listing.rebind",
- "listing.publish",
- "listing.archive",
- "market.refresh",
- "market.product.search",
- "market.listing.get",
- "basket.create",
- "basket.get",
- "basket.list",
- "basket.item.add",
- "basket.item.update",
- "basket.item.remove",
- "basket.adjustment.add",
- "basket.adjustment.remove",
- "basket.validate",
- "basket.quote.create",
- "trade.submit",
- "trade.get",
- "trade.list",
- "trade.app.list",
- "trade.app.export",
- "trade.rebind",
- "trade.accept",
- "trade.decline",
- "trade.cancel",
- "trade.status.get",
- "trade.event.list",
- "trade.event.watch",
- "validation.receipt.get",
- "validation.receipt.list",
- "validation.receipt.verify",
- ];
-
- const SUPPORTED_MUTATING_DRY_RUN_OPERATION_IDS: &[&str] = &[
- "workspace.init",
- "account.create",
- "account.import",
- "account.attach_secret",
- "account.remove",
- "account.selection.update",
- "account.selection.clear",
- "store.init",
- "store.backup.create",
- "store.backup.restore",
- "sync.pull",
- "sync.push",
- "transport.profile.set",
- "transport.outbox.push",
- "mesh.scope.set",
- "farm.create",
- "farm.rebind",
- "farm.profile.update",
- "farm.location.set",
- "farm.location.clear",
- "farm.fulfillment.update",
- "farm.publish",
- "listing.create",
- "listing.app.export",
- "listing.update",
- "listing.rebind",
- "listing.publish",
- "listing.archive",
- "market.refresh",
- "basket.create",
- "basket.item.add",
- "basket.item.update",
- "basket.item.remove",
- "basket.adjustment.add",
- "basket.adjustment.remove",
- "basket.quote.create",
- "trade.submit",
- "trade.app.export",
- "trade.rebind",
- "trade.accept",
- "trade.decline",
- "trade.cancel",
- ];
-
- const INTENTIONALLY_UNSUPPORTED_MUTATING_DRY_RUN_OPERATION_IDS: &[&str] = &[];
+ use super::{OPERATION_REGISTRY, get_operation};
#[test]
- fn registry_contains_exact_target_operation_set() {
- let actual = operation_ids();
- let expected = EXPECTED_OPERATION_IDS
+ fn registry_matches_runtime_contract_v1_catalog_exactly() {
+ let actual = OPERATION_REGISTRY
.iter()
- .copied()
+ .map(|operation| operation.operation_id)
+ .collect::<BTreeSet<_>>();
+ let expected = RUNTIME_OPERATION_DESCRIPTORS_V1
+ .iter()
+ .map(|descriptor| descriptor.operation_id.as_str())
.collect::<BTreeSet<_>>();
assert_eq!(actual, expected);
- assert_eq!(OPERATION_REGISTRY.len(), 84);
}
#[test]
- fn registry_identity_fields_are_consistent() {
- let mut operation_ids = BTreeSet::new();
- let mut cli_paths = BTreeSet::new();
- let mut mcp_tools = BTreeSet::new();
-
+ fn generated_registry_identity_fields_are_consistent() {
for operation in OPERATION_REGISTRY {
- assert!(operation_ids.insert(operation.operation_id));
- assert!(cli_paths.insert(operation.cli_path));
- assert!(mcp_tools.insert(operation.mcp_tool));
assert_eq!(operation.operation_id, operation.json_kind);
+ assert_eq!(
+ operation.operation_id,
+ operation.runtime_operation_id.as_str()
+ );
assert_eq!(operation.mcp_tool, operation.operation_id.replace('.', "_"));
assert!(operation.cli_path.starts_with("radroots "));
assert_eq!(
operation.namespace,
operation.operation_id.split('.').next().unwrap()
);
- assert_eq!(
- operation.rust_request,
- format!("{}Request", pascal_case(operation.operation_id))
- );
- assert_eq!(
- operation.rust_result,
- format!("{}Result", pascal_case(operation.operation_id))
- );
assert!(operation.supports_json);
assert!(!operation.description.is_empty());
+ assert_eq!(
+ get_operation(operation.operation_id)
+ .expect("operation")
+ .operation_id,
+ operation.operation_id
+ );
}
}
#[test]
- fn registry_policy_invariants_hold() {
- let required = OPERATION_REGISTRY
- .iter()
- .filter(|operation| operation.approval_policy == ApprovalPolicy::Required)
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let expected_required = [
- "account.import",
- "account.attach_secret",
- "account.remove",
- "sync.push",
- "farm.rebind",
- "farm.location.clear",
- "farm.publish",
- "listing.rebind",
- "listing.publish",
- "listing.update",
- "listing.archive",
- "transport.profile.set",
- "transport.outbox.push",
- "mesh.scope.set",
- "trade.submit",
- "trade.rebind",
- "trade.accept",
- "trade.decline",
- "trade.cancel",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
-
- assert_eq!(required, expected_required);
-
+ fn generated_registry_uses_contract_policy() {
for operation in OPERATION_REGISTRY {
- if operation.mutates {
- assert!(operation.supports_dry_run, "{}", operation.operation_id);
+ if operation.requires_approval() {
+ assert!(
+ matches!(
+ operation.descriptor.risk,
+ OperationRiskV1::High | OperationRiskV1::Critical
+ ),
+ "{}",
+ operation.operation_id
+ );
}
-
- if operation.approval_policy == ApprovalPolicy::Required {
+ if operation.mutates() {
+ assert!(operation.supports_dry_run(), "{}", operation.operation_id);
assert!(
- matches!(operation.risk_level, RiskLevel::High | RiskLevel::Critical),
+ operation.requires_idempotency(),
+ "{}",
+ operation.operation_id
+ );
+ } else {
+ assert!(
+ operation.forbids_idempotency(),
"{}",
operation.operation_id
);
}
}
}
-
- #[test]
- fn mutating_dry_run_registry_inventory_is_complete() {
- let advertised = OPERATION_REGISTRY
- .iter()
- .filter(|operation| operation.mutates && operation.supports_dry_run)
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let supported = SUPPORTED_MUTATING_DRY_RUN_OPERATION_IDS
- .iter()
- .copied()
- .collect::<BTreeSet<_>>();
- let unsupported = INTENTIONALLY_UNSUPPORTED_MUTATING_DRY_RUN_OPERATION_IDS
- .iter()
- .copied()
- .collect::<BTreeSet<_>>();
- let classified = supported
- .union(&unsupported)
- .copied()
- .collect::<BTreeSet<_>>();
-
- assert_eq!(advertised, classified);
- assert!(supported.is_disjoint(&unsupported));
- }
-
- #[test]
- fn registry_ndjson_support_is_explicit() {
- let actual = OPERATION_REGISTRY
- .iter()
- .filter(|operation| operation.supports_ndjson)
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let expected = [
- "health.status.get",
- "health.check.run",
- "config.get",
- "account.list",
- "transport.outbox.push",
- "farm.location.set",
- "sync.pull",
- "sync.push",
- "sync.watch",
- "listing.list",
- "market.refresh",
- "market.product.search",
- "basket.list",
- "trade.list",
- "trade.event.list",
- "validation.receipt.list",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
-
- assert_eq!(actual, expected);
- }
-
- #[test]
- fn registry_network_requirements_are_explicit() {
- let external = OPERATION_REGISTRY
- .iter()
- .filter(|operation| {
- matches!(
- network_requirement(operation.operation_id),
- NetworkRequirement::External { .. }
- )
- })
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let expected = [
- "sync.pull",
- "sync.push",
- "sync.watch",
- "market.refresh",
- "farm.publish",
- "listing.publish",
- "listing.update",
- "listing.archive",
- "trade.submit",
- "trade.accept",
- "trade.decline",
- "trade.cancel",
- "transport.outbox.push",
- "trade.event.list",
- "validation.receipt.get",
- "validation.receipt.list",
- "validation.receipt.verify",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
-
- assert_eq!(external, expected);
- }
-
- #[test]
- fn registry_local_signer_requirements_are_explicit() {
- let signed = OPERATION_REGISTRY
- .iter()
- .filter(|operation| requires_local_signer_mode(operation.operation_id))
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let expected = [
- "sync.push",
- "trade.submit",
- "trade.accept",
- "trade.decline",
- "trade.cancel",
- "transport.outbox.push",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
-
- assert_eq!(signed, expected);
- }
-
- #[test]
- fn registry_delivery_capable_transport_profile_requirements_are_explicit() {
- let publish = OPERATION_REGISTRY
- .iter()
- .filter(|operation| requires_delivery_capable_transport_profile(operation.operation_id))
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let expected = [
- "transport.outbox.push",
- "farm.publish",
- "listing.publish",
- "listing.update",
- "listing.archive",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
-
- assert_eq!(publish, expected);
- }
-
- #[test]
- fn deferred_namespaces_are_absent() {
- let namespaces = OPERATION_REGISTRY
- .iter()
- .map(|operation| operation.namespace)
- .collect::<BTreeSet<_>>();
-
- assert!(!namespaces.contains("product"));
- assert!(!namespaces.contains("message"));
- assert!(!namespaces.contains("approval"));
- assert!(!namespaces.contains("agent"));
- assert!(!namespaces.contains("runtime"));
- assert!(!namespaces.contains("job"));
- }
-
- #[test]
- fn roles_are_assigned_to_marketplace_operations() {
- assert_eq!(
- get_operation("listing.publish").unwrap().role,
- OperationRole::Seller
- );
- assert_eq!(
- get_operation("basket.quote.create").unwrap().role,
- OperationRole::Buyer
- );
- assert_eq!(
- get_operation("trade.list").unwrap().role,
- OperationRole::Any
- );
- }
-
- fn operation_ids() -> BTreeSet<&'static str> {
- OPERATION_REGISTRY
- .iter()
- .map(|operation| operation.operation_id)
- .collect()
- }
-
- fn pascal_case(operation_id: &str) -> String {
- operation_id
- .split('.')
- .flat_map(|part| part.split('_'))
- .map(|part| {
- let mut chars = part.chars();
- let first = chars.next().unwrap().to_ascii_uppercase();
- format!("{first}{}", chars.as_str())
- })
- .collect()
- }
}
diff --git a/src/registry/signer.rs b/src/registry/signer.rs
@@ -1,17 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const SIGNER_STATUS_GET: OperationSpec = operation!(
- "signer.status.get",
- "radroots signer status get",
- "signer",
- "signer_status_get",
- "SignerStatusGetRequest",
- "SignerStatusGetResult",
- "Get signer availability and permission status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
diff --git a/src/registry/store.rs b/src/registry/store.rs
@@ -1,81 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const STORE_INIT: OperationSpec = operation!(
- "store.init",
- "radroots store init",
- "store",
- "store_init",
- "StoreInitRequest",
- "StoreInitResult",
- "Initialize local store.",
- Any,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const STORE_STATUS_GET: OperationSpec = operation!(
- "store.status.get",
- "radroots store status get",
- "store",
- "store_status_get",
- "StoreStatusGetRequest",
- "StoreStatusGetResult",
- "Read local store status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const STORE_EXPORT: OperationSpec = operation!(
- "store.export",
- "radroots store export",
- "store",
- "store_export",
- "StoreExportRequest",
- "StoreExportResult",
- "Export local store data according to filters/policy.",
- Any,
- false,
- Conditional,
- Medium,
- false,
- false
-);
-
-pub const STORE_BACKUP_CREATE: OperationSpec = operation!(
- "store.backup.create",
- "radroots store backup create",
- "store",
- "store_backup_create",
- "StoreBackupCreateRequest",
- "StoreBackupCreateResult",
- "Create local store backup.",
- Any,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const STORE_BACKUP_RESTORE: OperationSpec = operation!(
- "store.backup.restore",
- "radroots store backup restore sdk-store-backup",
- "store",
- "store_backup_restore",
- "StoreBackupRestoreRequest",
- "StoreBackupRestoreResult",
- "Restore SDK canonical store backup.",
- Any,
- true,
- Conditional,
- High,
- false,
- true
-);
diff --git a/src/registry/sync.rs b/src/registry/sync.rs
@@ -1,65 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const SYNC_STATUS_GET: OperationSpec = operation!(
- "sync.status.get",
- "radroots sync status get",
- "sync",
- "sync_status_get",
- "SyncStatusGetRequest",
- "SyncStatusGetResult",
- "Read sync status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const SYNC_PULL: OperationSpec = operation!(
- "sync.pull",
- "radroots sync pull",
- "sync",
- "sync_pull",
- "SyncPullRequest",
- "SyncPullResult",
- "Pull remote updates into local store.",
- Any,
- true,
- None,
- Medium,
- true,
- true
-);
-
-pub const SYNC_PUSH: OperationSpec = operation!(
- "sync.push",
- "radroots sync push",
- "sync",
- "sync_push",
- "SyncPushRequest",
- "SyncPushResult",
- "Push local signed updates to relays.",
- Any,
- true,
- Required,
- High,
- true,
- true
-);
-
-pub const SYNC_WATCH: OperationSpec = operation!(
- "sync.watch",
- "radroots sync watch",
- "sync",
- "sync_watch",
- "SyncWatchRequest",
- "SyncWatchResult",
- "Stream sync events.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
diff --git a/src/registry/trade.rs b/src/registry/trade.rs
@@ -1,193 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const TRADE_SUBMIT: OperationSpec = operation!(
- "trade.submit",
- "radroots trade submit",
- "trade",
- "trade_submit",
- "TradeSubmitRequest",
- "TradeSubmitResult",
- "Submit quoted basket as a trade.",
- Buyer,
- true,
- Required,
- Critical,
- false,
- true
-);
-
-pub const TRADE_GET: OperationSpec = operation!(
- "trade.get",
- "radroots trade get",
- "trade",
- "trade_get",
- "TradeGetRequest",
- "TradeGetResult",
- "Get trade details.",
- Buyer,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const TRADE_LIST: OperationSpec = operation!(
- "trade.list",
- "radroots trade list",
- "trade",
- "trade_list",
- "TradeListRequest",
- "TradeListResult",
- "List trades.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const TRADE_APP_LIST: OperationSpec = operation!(
- "trade.app.list",
- "radroots trade app list",
- "trade",
- "trade_app_list",
- "TradeAppListRequest",
- "TradeAppListResult",
- "List app-authored shared local trade records.",
- Buyer,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const TRADE_APP_EXPORT: OperationSpec = operation!(
- "trade.app.export",
- "radroots trade app export",
- "trade",
- "trade_app_export",
- "TradeAppExportRequest",
- "TradeAppExportResult",
- "Export an app-authored shared trade record as a CLI draft.",
- Buyer,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const TRADE_REBIND: OperationSpec = operation!(
- "trade.rebind",
- "radroots trade rebind",
- "trade",
- "trade_rebind",
- "TradeRebindRequest",
- "TradeRebindResult",
- "Rebind a local trade draft to an explicit buyer actor.",
- Buyer,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const TRADE_ACCEPT: OperationSpec = operation!(
- "trade.accept",
- "radroots trade accept",
- "trade",
- "trade_accept",
- "TradeAcceptRequest",
- "TradeAcceptResult",
- "Accept a buyer trade request.",
- Seller,
- true,
- Required,
- Critical,
- false,
- true
-);
-
-pub const TRADE_DECLINE: OperationSpec = operation!(
- "trade.decline",
- "radroots trade decline",
- "trade",
- "trade_decline",
- "TradeDeclineRequest",
- "TradeDeclineResult",
- "Decline a buyer trade request.",
- Seller,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const TRADE_CANCEL: OperationSpec = operation!(
- "trade.cancel",
- "radroots trade cancel",
- "trade",
- "trade_cancel",
- "TradeCancelRequest",
- "TradeCancelResult",
- "Withdraw a buyer trade before agreement finalization.",
- Buyer,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const TRADE_STATUS_GET: OperationSpec = operation!(
- "trade.status.get",
- "radroots trade status get",
- "trade",
- "trade_status_get",
- "TradeStatusGetRequest",
- "TradeStatusGetResult",
- "Get SDK-projected trade status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const TRADE_EVENT_LIST: OperationSpec = operation!(
- "trade.event.list",
- "radroots trade event list",
- "trade",
- "trade_event_list",
- "TradeEventListRequest",
- "TradeEventListResult",
- "List trade events.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const TRADE_EVENT_WATCH: OperationSpec = operation!(
- "trade.event.watch",
- "radroots trade event watch",
- "trade",
- "trade_event_watch",
- "TradeEventWatchRequest",
- "TradeEventWatchResult",
- "Report deferred trade event watch status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
diff --git a/src/registry/transport.rs b/src/registry/transport.rs
@@ -1,81 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const TRANSPORT_PROFILE_GET: OperationSpec = operation!(
- "transport.profile.get",
- "radroots transport profile get",
- "transport",
- "transport_profile_get",
- "TransportProfileGetRequest",
- "TransportProfileGetResult",
- "Read the active transport profile.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const TRANSPORT_PROFILE_SET: OperationSpec = operation!(
- "transport.profile.set",
- "radroots transport profile set --kind local-only",
- "transport",
- "transport_profile_set",
- "TransportProfileSetRequest",
- "TransportProfileSetResult",
- "Write the active transport profile.",
- Any,
- true,
- Required,
- High,
- false,
- true
-);
-
-pub const TRANSPORT_STATUS: OperationSpec = operation!(
- "transport.status",
- "radroots transport status",
- "transport",
- "transport_status",
- "TransportStatusRequest",
- "TransportStatusResult",
- "Read transport implementation readiness.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const TRANSPORT_OUTBOX_STATUS: OperationSpec = operation!(
- "transport.outbox.status",
- "radroots transport outbox status",
- "transport",
- "transport_outbox_status",
- "TransportOutboxStatusRequest",
- "TransportOutboxStatusResult",
- "Read SDK transport outbox status.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const TRANSPORT_OUTBOX_PUSH: OperationSpec = operation!(
- "transport.outbox.push",
- "radroots transport outbox push",
- "transport",
- "transport_outbox_push",
- "TransportOutboxPushRequest",
- "TransportOutboxPushResult",
- "Push ready SDK outbox events through the active transport profile.",
- Any,
- true,
- Required,
- High,
- true,
- true
-);
diff --git a/src/registry/validation.rs b/src/registry/validation.rs
@@ -1,49 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const VALIDATION_RECEIPT_GET: OperationSpec = operation!(
- "validation.receipt.get",
- "radroots validation receipt get",
- "validation",
- "validation_receipt_get",
- "ValidationReceiptGetRequest",
- "ValidationReceiptGetResult",
- "Fetch and inspect one validation receipt event.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
-
-pub const VALIDATION_RECEIPT_LIST: OperationSpec = operation!(
- "validation.receipt.list",
- "radroots validation receipt list",
- "validation",
- "validation_receipt_list",
- "ValidationReceiptListRequest",
- "ValidationReceiptListResult",
- "List validation receipts for one trade.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
-
-pub const VALIDATION_RECEIPT_VERIFY: OperationSpec = operation!(
- "validation.receipt.verify",
- "radroots validation receipt verify",
- "validation",
- "validation_receipt_verify",
- "ValidationReceiptVerifyRequest",
- "ValidationReceiptVerifyResult",
- "Verify validation receipt tags, payload, and proof binding.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
diff --git a/src/registry/workspace.rs b/src/registry/workspace.rs
@@ -1,33 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const WORKSPACE_INIT: OperationSpec = operation!(
- "workspace.init",
- "radroots workspace init",
- "workspace",
- "workspace_init",
- "WorkspaceInitRequest",
- "WorkspaceInitResult",
- "Initialize a local workspace/profile.",
- Any,
- true,
- None,
- Medium,
- false,
- true
-);
-
-pub const WORKSPACE_GET: OperationSpec = operation!(
- "workspace.get",
- "radroots workspace get",
- "workspace",
- "workspace_get",
- "WorkspaceGetRequest",
- "WorkspaceGetResult",
- "Read workspace/profile configuration and current context.",
- Any,
- false,
- None,
- Low,
- false,
- false
-);
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -1263,13 +1263,11 @@ fn resolve_rhi_config(
.and_then(|rhi| rhi.require_cryptographic_proof)
{
value
- } else if let Some(value) = workspace_config
- .and_then(|config| config.rhi.as_ref())
- .and_then(|rhi| rhi.require_cryptographic_proof)
- {
- value
} else {
- false
+ workspace_config
+ .and_then(|config| config.rhi.as_ref())
+ .and_then(|rhi| rhi.require_cryptographic_proof)
+ .unwrap_or_default()
};
Ok(RhiConfig {
@@ -2159,6 +2157,10 @@ fn validate_logging_output_contract(
Ok(())
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "configuration precedence keeps each source explicit"
+)]
fn resolve_bool_pair(
positive_flag: bool,
negative_flag: bool,
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -214,7 +214,7 @@ fn rebind_inner(
actions: if dry_run {
vec![farm_rebind_live_action(args.selector.as_str())]
} else {
- vec!["radroots farm readiness check".to_owned()]
+ vec!["radroots farm get".to_owned()]
},
})
}
@@ -632,7 +632,7 @@ fn transport_farm_publish_readiness(
reason: Some("farm publish requires a configured Nostr transport profile".to_owned()),
missing: vec!["Configured Nostr transport profile".to_owned()],
actions: vec![
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
.to_owned(),
],
};
@@ -650,7 +650,7 @@ fn transport_farm_publish_readiness(
executable: false,
reason: Some(error.to_string()),
missing: vec!["Remote signer binding".to_owned()],
- actions: vec!["radroots signer status get".to_owned()],
+ actions: vec!["radroots signer status".to_owned()],
};
}
return FarmPublishReadiness {
@@ -877,6 +877,10 @@ struct FarmPublishEventDraft {
event: FarmPublishEventView,
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "publish view construction mirrors the V1 output contract fields"
+)]
fn missing_publish_view(
config: &RuntimeConfig,
scope: FarmConfigScope,
@@ -917,6 +921,10 @@ fn missing_publish_view(
}
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "publish view construction mirrors the V1 output contract fields"
+)]
fn base_publish_view(
state: &str,
config: &RuntimeConfig,
@@ -1058,6 +1066,10 @@ fn not_submitted_component(
preview_component(rpc_method, event_kind, idempotency_key, args, event)
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "publish view construction mirrors the V1 output contract fields"
+)]
fn binding_error_publish_view(
config: &RuntimeConfig,
args: &FarmPublishArgs,
@@ -1070,7 +1082,7 @@ fn binding_error_publish_view(
) -> FarmPublishView {
let reason = error.reason();
let state = "unconfigured".to_owned();
- let actions = vec!["run radroots signer status get".to_owned()];
+ let actions = vec!["run radroots signer status".to_owned()];
base_publish_view(
state.as_str(),
config,
@@ -1118,6 +1130,10 @@ fn sdk_farm_publish_input(
})
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "publish view construction mirrors the V1 output contract fields"
+)]
fn sdk_prepared_publish_view(
config: &RuntimeConfig,
args: &FarmPublishArgs,
@@ -1159,6 +1175,10 @@ fn sdk_prepared_publish_view(
)
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "publish view construction mirrors the V1 output contract fields"
+)]
fn sdk_enqueued_publish_view(
config: &RuntimeConfig,
args: &FarmPublishArgs,
@@ -1607,7 +1627,7 @@ fn init_document(
&& document.selection.account != account.record.account_id.to_string()
{
let message = format!(
- "account mismatch: farm config is bound to seller account `{}`; use `radroots --approval-token approve farm rebind {}` to change the farm-bound seller account",
+ "account mismatch: farm config is bound to seller account `{}`; select account `{}` before updating this farm config",
document.selection.account, account.record.account_id
);
return Err(account::AccountRuntimeFailure::mismatch_with_detail(
@@ -1790,7 +1810,7 @@ fn farm_setup_actions(
document: &FarmConfigDocument,
account: Option<&AccountRecordView>,
) -> Vec<String> {
- let mut actions = vec!["radroots farm readiness check".to_owned()];
+ let mut actions = vec!["radroots farm get".to_owned()];
if account.is_none() {
actions.extend(farm_bound_seller_recovery_actions("<selector>"));
return actions;
@@ -1823,11 +1843,11 @@ pub(crate) fn farm_rebind_recovery_actions(selector: &str) -> Vec<String> {
}
pub(crate) fn farm_rebind_dry_run_action(selector: &str) -> String {
- format!("radroots --dry-run farm rebind {selector}")
+ format!("radroots --dry-run account select {selector}")
}
pub(crate) fn farm_rebind_live_action(selector: &str) -> String {
- format!("radroots --approval-token approve farm rebind {selector}")
+ format!("radroots account select {selector}")
}
fn account_recovery_actions() -> Vec<String> {
@@ -1982,7 +2002,7 @@ fn ensure_farm_location(document: &mut FarmConfigDocument) -> &mut RadrootsFarmP
document
.farm
.location
- .get_or_insert_with(|| RadrootsFarmPublicLocation {
+ .get_or_insert(RadrootsFarmPublicLocation {
primary,
city,
region,
diff --git a/src/runtime/find.rs b/src/runtime/find.rs
@@ -57,14 +57,14 @@ pub fn search(config: &RuntimeConfig, args: &FindQueryArgs) -> Result<FindView,
results: Vec::new(),
hyf: None,
reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
+ actions: vec!["radroots store inspect".to_owned()],
});
}
refresh_market_if_needed(config)?;
let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?);
let freshness =
- freshness_for_scope_from_executor(config, db.executor(), RelayIngestScope::MarketRefresh)?;
+ freshness_for_scope_from_executor(config, db.executor(), RelayIngestScope::MarketPull)?;
let applied_query_rewrite = attempt_query_rewrite(config, query.as_str(), &args.query);
let effective_query_terms = applied_query_rewrite
.as_ref()
@@ -130,7 +130,7 @@ pub fn search(config: &RuntimeConfig, args: &FindQueryArgs) -> Result<FindView,
let (state, reason, actions) = if results.is_empty() {
let actions = if freshness.state == "never" {
- vec!["radroots sync status get".to_owned()]
+ vec!["radroots sync status".to_owned()]
} else {
Vec::new()
};
@@ -164,7 +164,7 @@ fn refresh_market_if_needed(config: &RuntimeConfig) -> Result<(), RuntimeError>
}
let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
let freshness =
- freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketRefresh)?;
+ freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketPull)?;
if freshness_requires_refresh(&freshness) {
let _ = market_refresh(config)?;
}
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -272,7 +272,8 @@ impl From<ListingValidationIssueView> for ListingDraftValidationError {
pub enum ListingMutationOperation {
Publish,
Update,
- Archive,
+ Pause,
+ Withdraw,
}
impl ListingMutationOperation {
@@ -280,7 +281,16 @@ impl ListingMutationOperation {
match self {
Self::Publish => "publish",
Self::Update => "update",
- Self::Archive => "archive",
+ Self::Pause => "pause",
+ Self::Withdraw => "withdraw",
+ }
+ }
+
+ fn listing_status(self) -> Option<&'static str> {
+ match self {
+ Self::Pause => Some("paused"),
+ Self::Withdraw => Some("withdrawn"),
+ Self::Publish | Self::Update => None,
}
}
}
@@ -295,7 +305,7 @@ pub fn scaffold(
append_listing_local_work(config, output_path.as_path(), &draft)?;
let mut actions = vec![format!(
- "radroots listing validate {}",
+ "radroots listing publish {}",
output_path.display()
)];
if let Some(action) = &defaults.farm_next_action {
@@ -328,7 +338,7 @@ pub fn scaffold_preflight(
validate_listing_output_target(&output_path)?;
let mut actions = vec![format!(
- "radroots listing validate {}",
+ "radroots listing publish {}",
output_path.display()
)];
if let Some(action) = &defaults.farm_next_action {
@@ -520,13 +530,14 @@ fn validate_listing_output_target(output_path: &Path) -> Result<(), RuntimeError
output_path.display()
)));
}
- if let Some(parent) = output_path.parent() {
- if parent.exists() && !parent.is_dir() {
- return Err(RuntimeError::Config(format!(
- "listing draft parent {} is not a directory",
- parent.display()
- )));
- }
+ if let Some(parent) = output_path.parent()
+ && parent.exists()
+ && !parent.is_dir()
+ {
+ return Err(RuntimeError::Config(format!(
+ "listing draft parent {} is not a directory",
+ parent.display()
+ )));
}
Ok(())
}
@@ -893,7 +904,7 @@ pub fn app_record_export(
"app-authored listing record exported as a CLI listing draft".to_owned()
}),
actions: vec![
- format!("radroots listing validate {}", output_path.display()),
+ format!("radroots listing publish {}", output_path.display()),
format!("radroots listing publish {}", output_path.display()),
],
})
@@ -997,13 +1008,9 @@ fn rebind_inner(
"listing seller actor binding updated".to_owned()
}),
actions: if dry_run {
- vec![format!(
- "radroots --approval-token approve listing rebind {} {}",
- args.file.display(),
- args.selector
- )]
+ vec![format!("radroots account select {}", args.selector)]
} else {
- vec![format!("radroots listing validate {}", args.file.display())]
+ vec![format!("radroots listing publish {}", args.file.display())]
},
})
}
@@ -1616,7 +1623,7 @@ pub fn get(
refresh_market_listing_if_needed(config)?;
let freshness = if config.local.replica_store_path.exists() {
let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketRefresh)?
+ freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketPull)?
} else {
missing_freshness()
};
@@ -1644,7 +1651,7 @@ pub fn get(
price: None,
provenance,
reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
+ actions: vec!["radroots store inspect".to_owned()],
});
}
@@ -1673,7 +1680,7 @@ pub fn get(
)),
actions: vec![
"radroots sync pull".to_owned(),
- format!("radroots market product search {}", args.key),
+ format!("radroots market search {}", args.key),
],
});
};
@@ -1737,7 +1744,7 @@ fn refresh_market_listing_if_needed(config: &RuntimeConfig) -> Result<(), Runtim
}
let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
let freshness =
- freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketRefresh)?;
+ freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketPull)?;
if crate::runtime::sync::freshness_requires_refresh(&freshness) {
let _ = market_refresh(config)?;
}
@@ -2114,11 +2121,18 @@ pub fn update(
mutate(config, args, ListingMutationOperation::Update)
}
-pub fn archive(
+pub fn pause(
+ config: &RuntimeConfig,
+ args: &ListingMutationArgs,
+) -> Result<ListingMutationView, CliSdkAdapterError> {
+ mutate(config, args, ListingMutationOperation::Pause)
+}
+
+pub fn withdraw(
config: &RuntimeConfig,
args: &ListingMutationArgs,
) -> Result<ListingMutationView, CliSdkAdapterError> {
- mutate(config, args, ListingMutationOperation::Archive)
+ mutate(config, args, ListingMutationOperation::Withdraw)
}
fn mutate(
@@ -2158,10 +2172,10 @@ fn mutate(
})?;
ensure_listing_bound_account(config, &canonical, args.file.as_path())?;
- if matches!(operation, ListingMutationOperation::Archive) {
+ if let Some(status) = operation.listing_status() {
canonical.listing.availability = Some(RadrootsListingAvailability::Status {
status: RadrootsListingStatus::Other {
- value: "archived".to_owned(),
+ value: status.to_owned(),
},
});
}
@@ -2237,7 +2251,7 @@ fn scaffold_contents(draft: &ListingDraftDocument) -> Result<String, RuntimeErro
RuntimeError::Config(format!("failed to render listing draft: {error}"))
})?;
Ok(format!(
- "# radroots listing draft v1\n# this scaffold applies selected farm defaults and provided product inputs when available\n# review any remaining empty fields, then run `radroots listing validate <file>`\n\n{toml}"
+ "# radroots listing draft v1\n# this scaffold applies selected farm defaults and provided product inputs when available\n# review any remaining empty fields, then run `radroots listing publish <file>`\n\n{toml}"
))
}
@@ -2953,7 +2967,7 @@ fn authoring_defaults(config: &RuntimeConfig) -> Result<ListingAuthoringDefaults
defaults.farm_next_action = None;
defaults.farm_reason = None;
} else {
- defaults.farm_next_action = Some("radroots farm readiness check".to_owned());
+ defaults.farm_next_action = Some("radroots farm get".to_owned());
defaults.farm_reason = Some(
"selected farm draft is missing delivery or location defaults; those fields were left blank"
.to_owned(),
diff --git a/src/runtime/mesh.rs b/src/runtime/mesh.rs
@@ -91,6 +91,6 @@ fn scope_view(scope: &str, state: &str) -> MeshScopeView {
implementation: "unavailable".to_owned(),
message: "Mesh delivery is disabled unless a Reticulum scope is explicitly configured"
.to_owned(),
- actions: vec!["radroots mesh policy check".to_owned()],
+ actions: vec!["radroots diagnostics inspect".to_owned()],
}
}
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -69,7 +69,7 @@ use serde_json::{Value, json};
use crate::cli::global::{
OrderDraftCreateArgs, RecordLookupArgs, TradeAppRecordExportArgs, TradeCancelArgs,
- TradeDecisionArg, TradeDecisionArgs, TradeRebindArgs, TradeStatusArgs, TradeSubmitArgs,
+ TradeDecisionArg, TradeDecisionArgs, TradeRebindArgs, TradeRequestArgs, TradeStatusArgs,
};
use crate::runtime::RuntimeError;
use crate::runtime::account;
@@ -96,13 +96,13 @@ use self::sdk_status::sdk_order_status_view;
const ORDER_DRAFT_KIND: &str = "order_draft_v1";
const ORDER_SOURCE: &str = "local trade drafts · local first";
const ORDER_APP_RECORD_SOURCE: &str = "app-authored shared local trade records";
-const ORDER_SUBMIT_SOURCE: &str = "SDK trade submit · local key";
+const ORDER_SUBMIT_SOURCE: &str = "SDK trade request · local key";
const ORDER_DECISION_SOURCE: &str = "SDK trade decision · local key";
const ORDER_CANCELLATION_SOURCE: &str = "SDK trade cancellation · local key";
const ORDER_EVENT_LIST_SOURCE: &str = "shared Nostr transport fetch · selected seller identity";
const ORDER_STATUS_SDK_SOURCE: &str = "SDK local trade projection";
const ORDER_EVENT_LIST_RELAY_ACTION: &str =
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com";
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com";
const ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
const ORDER_BUYER_ACTOR_SOURCE_REBIND: &str = "order_rebind";
const ORDER_APP_RECORD_LIST_LIMIT: u32 = 500;
@@ -851,7 +851,7 @@ pub fn app_record_export(
app_order.loaded.document.order.order_id
),
format!(
- "radroots trade submit {}",
+ "radroots trade request {}",
app_order.loaded.document.order.order_id
),
],
@@ -860,7 +860,7 @@ pub fn app_record_export(
pub fn submit(
config: &RuntimeConfig,
- args: &TradeSubmitArgs,
+ args: &TradeRequestArgs,
) -> Result<OrderSubmitView, CliSdkAdapterError> {
let file = draft_lookup_path(config, args.key.as_str());
let (loaded, source_issues) = if file.exists() {
@@ -1075,7 +1075,7 @@ fn rebind_inner(
.to_owned(),
),
actions: vec![
- format!("radroots trade status get {from_order_id}"),
+ format!("radroots trade get {from_order_id}"),
"radroots basket quote create <basket-id>".to_owned(),
],
});
@@ -1140,10 +1140,7 @@ fn rebind_inner(
"order buyer actor binding updated".to_owned()
}),
actions: if dry_run {
- vec![format!(
- "radroots --approval-token approve order rebind {} {}",
- args.key, args.selector
- )]
+ vec![format!("radroots account select {}", args.selector)]
} else {
vec![format!("radroots trade get {to_order_id}")]
},
@@ -1432,7 +1429,7 @@ fn sdk_trade_decision_outcome_view(
"dry run requested; seller trade {} publication skipped",
args.decision.command()
));
- view.actions = vec![format!("radroots trade status get {}", status.order_id)];
+ view.actions = vec![format!("radroots trade get {}", status.order_id)];
view
}
TradeMutationOutcome::Enqueued { receipt } => {
@@ -1461,7 +1458,7 @@ fn sdk_trade_cancellation_outcome_view(
view.target_transport_endpoints = config.transport.nostr_relay_urls.clone();
view.reason =
Some("dry run requested; buyer trade cancellation publication skipped".to_owned());
- view.actions = vec![format!("radroots trade status get {}", status.order_id)];
+ view.actions = vec![format!("radroots trade get {}", status.order_id)];
view
}
TradeMutationOutcome::Enqueued { receipt } => {
@@ -2028,7 +2025,7 @@ fn sdk_mutation_state_label(state: &SdkMutationState) -> &'static str {
fn sdk_order_push_recovery_actions() -> Vec<String> {
vec![
"radroots sync push".to_owned(),
- "radroots sync status get".to_owned(),
+ "radroots sync status".to_owned(),
]
}
@@ -2093,7 +2090,7 @@ fn resolve_order_listing(
if !config.local.replica_store_path.exists() {
return Err(RuntimeError::Config(format!(
- "trade listing lookup `{listing_lookup}` requires local market data; run `radroots store init` and `radroots market refresh` before creating a trade from a listing"
+ "trade listing lookup `{listing_lookup}` requires local market data; run `radroots store inspect` and `radroots market pull` before creating a trade from a listing"
)));
}
@@ -2101,24 +2098,24 @@ fn resolve_order_listing(
let rows = db.trade_product_lookup(listing_lookup)?;
match rows.len() {
0 => Err(RuntimeError::Config(format!(
- "listing `{listing_lookup}` is not available in the local replica; run `radroots market refresh` or pass `--listing-addr`"
+ "listing `{listing_lookup}` is not available in the local replica; run `radroots market pull` or pass `--listing-addr`"
))),
1 => {
let row = rows.into_iter().next().expect("one row");
let economics_product = ResolvedOrderEconomicsProduct::from_summary(&row);
let listing_addr = normalize_optional(row.listing_addr.as_deref()).ok_or_else(|| {
RuntimeError::Config(format!(
- "listing `{listing_lookup}` is missing a canonical listing address; run `radroots market refresh` or pass `--listing-addr`"
+ "listing `{listing_lookup}` is missing a canonical listing address; run `radroots market pull` or pass `--listing-addr`"
))
})?;
let parsed = parse_listing_addr(listing_addr.as_str()).map_err(|error| {
RuntimeError::Config(format!(
- "listing `{listing_lookup}` has invalid listing_addr: {error}; run `radroots market refresh` or pass `--listing-addr`"
+ "listing `{listing_lookup}` has invalid listing_addr: {error}; run `radroots market pull` or pass `--listing-addr`"
))
})?;
if parsed.kind != KIND_LISTING {
return Err(RuntimeError::Config(format!(
- "listing `{listing_lookup}` listing_addr must reference a public NIP-99 listing; run `radroots market refresh` or pass `--listing-addr`"
+ "listing `{listing_lookup}` listing_addr must reference a public NIP-99 listing; run `radroots market pull` or pass `--listing-addr`"
)));
}
@@ -2129,7 +2126,7 @@ fn resolve_order_listing(
)?
.ok_or_else(|| {
RuntimeError::Config(format!(
- "listing `{listing_lookup}` is missing the latest listing event pointer; run `radroots market refresh` before creating a trade from this listing"
+ "listing `{listing_lookup}` is missing the latest listing event pointer; run `radroots market pull` before creating a trade from this listing"
))
})?;
let shared_provenance = resolve_shared_signed_listing_provenance(
@@ -2260,7 +2257,7 @@ fn listing_provenance_relays(
}
relays.extend(relay_provenance_relays_for_scope(
config,
- RelayIngestScope::MarketRefresh,
+ RelayIngestScope::MarketPull,
)?);
normalize_listing_relay_set(relays)
.map_err(|error| RuntimeError::Config(format!("listing provenance relays: {error}")))
@@ -3226,16 +3223,13 @@ fn app_order_record_summary(
vec![
format!("radroots trade get {}", document.order.order_id),
format!("radroots trade app export {}", record.record_id),
- format!("radroots trade submit {}", document.order.order_id),
+ format!("radroots trade request {}", document.order.order_id),
]
} else if app_order_issue_present(&issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
- vec![format!(
- "radroots trade status get {}",
- document.order.order_id
- )]
+ vec![format!("radroots trade get {}", document.order.order_id)]
} else if app_order_issue_present(&issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
vec![
- format!("radroots trade status get {}", document.order.order_id),
+ format!("radroots trade get {}", document.order.order_id),
"radroots trade app list".to_owned(),
]
} else {
@@ -3339,13 +3333,10 @@ fn app_order_export_failure_actions(
issues: &[OrderIssueView],
) -> Vec<String> {
if app_order_issue_present(issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
- vec![format!(
- "radroots trade status get {}",
- document.order.order_id
- )]
+ vec![format!("radroots trade get {}", document.order.order_id)]
} else if app_order_issue_present(issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
vec![
- format!("radroots trade status get {}", document.order.order_id),
+ format!("radroots trade get {}", document.order.order_id),
"radroots trade app list".to_owned(),
]
} else {
@@ -3370,13 +3361,14 @@ fn validate_order_export_output_target(output_path: &Path) -> Result<(), Runtime
output_path.display()
)));
}
- if let Some(parent) = output_path.parent() {
- if parent.exists() && !parent.is_dir() {
- return Err(RuntimeError::Config(format!(
- "trade draft parent {} is not a directory",
- parent.display()
- )));
- }
+ if let Some(parent) = output_path.parent()
+ && parent.exists()
+ && !parent.is_dir()
+ {
+ return Err(RuntimeError::Config(format!(
+ "trade draft parent {} is not a directory",
+ parent.display()
+ )));
}
Ok(())
}
@@ -3527,13 +3519,12 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
));
}
if let Some(seller_pubkey) = non_empty_string(document.order.seller_pubkey.clone())
+ && seller_pubkey != parsed.seller_pubkey
{
- if seller_pubkey != parsed.seller_pubkey {
- issues.push(issue(
- "order.seller_pubkey",
- "seller_pubkey must match listing_addr seller when both are set",
- ));
- }
+ issues.push(issue(
+ "order.seller_pubkey",
+ "seller_pubkey must match listing_addr seller when both are set",
+ ));
}
}
Err(error) => issues.push(issue(
@@ -3543,7 +3534,7 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
},
None => issues.push(issue(
"trade.listing_addr",
- "listing_addr is required before trade submit",
+ "listing_addr is required before trade request",
)),
}
@@ -3558,7 +3549,7 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
}
None => issues.push(issue(
"trade.listing_event_id",
- "latest active listing event id is required before trade submit; run `radroots market refresh` and create the trade from local market data",
+ "latest active listing event id is required before trade request; run `radroots market pull` and create the trade from local market data",
)),
}
@@ -3566,7 +3557,7 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
Ok(listing_relays) if listing_relays.is_empty() => issues.push(issue_with_code(
"listing_provenance_missing",
"trade.listing_relays",
- "listing relay provenance is required before trade submit; run `radroots market refresh` and create the trade from current local market data",
+ "listing relay provenance is required before trade request; run `radroots market pull` and create the trade from current local market data",
)),
Ok(_) => {}
Err(error) => issues.push(issue_with_code(
@@ -3579,7 +3570,7 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
if document.order.items.is_empty() {
issues.push(issue(
"order.items",
- "at least one order item is required before trade submit",
+ "at least one order item is required before trade request",
));
}
for (index, item) in document.order.items.iter().enumerate() {
@@ -3614,26 +3605,26 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
}
None => issues.push(issue(
"order.economics",
- "quote economics is required before trade submit; run `radroots basket quote create` from current local market data",
+ "quote economics is required before trade request; run `radroots basket quote create` from current local market data",
)),
}
if document.buyer_actor.account_id.trim().is_empty() {
issues.push(issue(
"buyer_actor.account_id",
- "buyer_actor account_id is required before trade submit",
+ "buyer_actor account_id is required before trade request",
));
}
if document.buyer_actor.pubkey.trim().is_empty() {
issues.push(issue(
"buyer_actor.pubkey",
- "buyer_actor pubkey is required before trade submit",
+ "buyer_actor pubkey is required before trade request",
));
}
if document.buyer_actor.source.trim().is_empty() {
issues.push(issue(
"buyer_actor.source",
- "buyer_actor source is required before trade submit",
+ "buyer_actor source is required before trade request",
));
} else if !matches!(
document.buyer_actor.source.as_str(),
@@ -3650,7 +3641,7 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
if document.order.buyer_pubkey.trim().is_empty() {
issues.push(issue(
"order.buyer_pubkey",
- "order buyer_pubkey is required before trade submit",
+ "order buyer_pubkey is required before trade request",
));
} else if !document
.order
@@ -3690,14 +3681,11 @@ fn actions_for_document(
issues: &[OrderIssueView],
) -> Vec<String> {
if app_order_issue_present(issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
- return vec![format!(
- "radroots trade status get {}",
- document.order.order_id
- )];
+ return vec![format!("radroots trade get {}", document.order.order_id)];
}
if app_order_issue_present(issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
return vec![
- format!("radroots trade status get {}", document.order.order_id),
+ format!("radroots trade get {}", document.order.order_id),
"radroots trade app list".to_owned(),
];
}
@@ -3931,7 +3919,7 @@ fn order_buyer_write_actor_context(
fn order_submit_app_signed_evidence_view(
config: &RuntimeConfig,
loaded: &LoadedOrderDraft,
- args: &TradeSubmitArgs,
+ args: &TradeRequestArgs,
issues: &[OrderIssueView],
) -> Option<OrderSubmitView> {
if let Some(issue) = app_order_issue(issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
@@ -4004,7 +3992,7 @@ fn order_submit_app_signed_evidence_view(
job: None,
issues: issues.to_vec(),
actions: vec![format!(
- "radroots trade status get {}",
+ "radroots trade get {}",
loaded.document.order.order_id
)],
});
@@ -4070,7 +4058,7 @@ fn order_submit_request_matches_draft(
fn order_submit_dry_run_view(
config: &RuntimeConfig,
loaded: &LoadedOrderDraft,
- args: &TradeSubmitArgs,
+ args: &TradeRequestArgs,
plan: TradeSubmitPlan,
target_transport_endpoints: Vec<String>,
) -> OrderSubmitView {
@@ -4110,7 +4098,7 @@ fn order_submit_dry_run_view(
job: None,
issues: Vec::new(),
actions: vec![format!(
- "radroots trade submit {}",
+ "radroots trade request {}",
loaded.document.order.order_id
)],
}
@@ -4169,7 +4157,7 @@ fn order_submit_listing_event_ptr(
fn propose_trade_via_sdk(
config: &RuntimeConfig,
loaded: &LoadedOrderDraft,
- args: &TradeSubmitArgs,
+ args: &TradeRequestArgs,
account: &account::AccountRecordView,
) -> Result<OrderSubmitView, CliSdkAdapterError> {
let actor = sdk_trade_actor(account, RadrootsActorRole::Buyer, "propose")?;
@@ -4200,7 +4188,7 @@ fn propose_trade_via_sdk(
fn sdk_trade_submit_outcome_view(
config: &RuntimeConfig,
loaded: &LoadedOrderDraft,
- args: &TradeSubmitArgs,
+ args: &TradeRequestArgs,
outcome: TradeMutationOutcome<TradeSubmitPlan, TradeSubmitReceipt>,
) -> OrderSubmitView {
match outcome {
@@ -4223,7 +4211,7 @@ fn sdk_trade_submit_outcome_view(
fn sdk_enqueued_order_submit_view(
config: &RuntimeConfig,
loaded: &LoadedOrderDraft,
- args: &TradeSubmitArgs,
+ args: &TradeRequestArgs,
enqueue: TradeSubmitReceipt,
push: Option<&PushOutboxReceipt>,
) -> OrderSubmitView {
@@ -4311,7 +4299,7 @@ fn sdk_order_submit_reason(
sdk_order_enqueue_retry_summary(enqueue)
)),
None => Some(format!(
- "{}; trade submit queued in SDK outbox; no ready SDK outbox event was pushed; {}",
+ "{}; trade request queued in SDK outbox; no ready SDK outbox event was pushed; {}",
sdk_order_enqueue_summary(enqueue),
sdk_order_enqueue_retry_summary(enqueue)
)),
@@ -4492,7 +4480,7 @@ fn validate_bound_order_buyer_account(
"attempted_buyer_account_id": attempted.record.account_id.to_string(),
"attempted_buyer_pubkey": attempted_pubkey,
"actions": [
- format!("radroots --account-id {account_id} trade submit {}", document.order.order_id),
+ format!("radroots --account-id {account_id} trade request {}", document.order.order_id),
format!("radroots trade rebind {} <selector>", document.order.order_id),
format!("radroots trade get {}", document.order.order_id),
],
diff --git a/src/runtime/order/sdk_status.rs b/src/runtime/order/sdk_status.rs
@@ -84,7 +84,7 @@ fn sdk_status_ambiguity_candidate_view(
let status_selector = TradeStatusRequest::locator_selector(&candidate.locator);
OrderStatusAmbiguityCandidateView {
locator: sdk_trade_locator_view(&candidate.locator),
- status_command: format!("radroots trade status get {status_selector}"),
+ status_command: format!("radroots trade get {status_selector}"),
status_selector,
}
}
@@ -319,7 +319,7 @@ mod tests {
assert_eq!(
view_json["actions"][0],
json!(format!(
- "radroots trade status get order-1@{}",
+ "radroots trade get order-1@{}",
root_event_id.as_str()
))
);
diff --git a/src/runtime/runtime_store.rs b/src/runtime/runtime_store.rs
@@ -129,6 +129,16 @@ fn shared_runtime_store_root_from_paths(paths: &PathsConfig) -> Result<PathBuf,
.map_err(|err| RuntimeError::Config(format!("resolve shared runtime-store root: {err}")))
}
+fn current_time_ms() -> Result<i64, RuntimeError> {
+ let duration = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map_err(|error| {
+ RuntimeError::Config(format!("system clock is before unix epoch: {error}"))
+ })?;
+ i64::try_from(duration.as_millis())
+ .map_err(|_| RuntimeError::Config("current timestamp exceeds i64 milliseconds".to_owned()))
+}
+
#[cfg(test)]
mod tests {
use std::path::PathBuf;
@@ -181,13 +191,3 @@ mod tests {
}
}
}
-
-fn current_time_ms() -> Result<i64, RuntimeError> {
- let duration = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|error| {
- RuntimeError::Config(format!("system clock is before unix epoch: {error}"))
- })?;
- i64::try_from(duration.as_millis())
- .map_err(|_| RuntimeError::Config("current timestamp exceeds i64 milliseconds".to_owned()))
-}
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -337,12 +337,12 @@ fn myc_nip46_signer_input(
binding.target_kind.as_str()
)));
}
- if let Some(managed_account_ref) = binding.managed_account_ref.as_deref() {
- if !myc_managed_account_ref_matches(managed_account_ref, actor_account_id, actor_pubkey) {
- return Err(RuntimeError::Config(format!(
- "signer.remote_nip46 managed_account_ref `{managed_account_ref}` does not match actor account or pubkey"
- )));
- }
+ if let Some(managed_account_ref) = binding.managed_account_ref.as_deref()
+ && !myc_managed_account_ref_matches(managed_account_ref, actor_account_id, actor_pubkey)
+ {
+ return Err(RuntimeError::Config(format!(
+ "signer.remote_nip46 managed_account_ref `{managed_account_ref}` does not match actor account or pubkey"
+ )));
}
let signer_session_ref = binding.signer_session_ref.as_deref().ok_or_else(|| {
RuntimeError::Config("signer.remote_nip46 signer_session_ref is missing".to_owned())
@@ -1181,7 +1181,7 @@ mod tests {
],
},
MigratedCliPathGuard {
- label: "trade submit",
+ label: "trade request",
path: "src/runtime/order.rs",
start: "fn canonical_order_request_payload_from_loaded(",
end: "fn sdk_trade_submit_outcome_view(",
@@ -1548,25 +1548,25 @@ mod tests {
let sdk_src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../sdk/crates/sdk/src");
let mut files = Vec::new();
collect_rs_files(sdk_src.as_path(), &mut files);
- let forbidden = [
- ("radroots_cli", "CLI crate identity"),
- ("domains/radroots/cli", "CLI mount path"),
- ("approval_token", "CLI approval-token UX"),
- ("OutputEnvelope", "CLI output envelope"),
- ("next_actions", "CLI next-action rendering"),
- ("exit_code", "CLI exit-code contract"),
- ("docs/", "repository docs path"),
- ("radroots store", "CLI command string"),
- ("radroots sync", "CLI command string"),
- ("radroots listing", "CLI command string"),
- ("radroots trade", "CLI command string"),
+ let forbidden = vec![
+ ("radroots_cli".to_owned(), "CLI crate identity"),
+ ("domains/radroots/cli".to_owned(), "CLI mount path"),
+ (["approval", "token"].join("_"), "retired approval string"),
+ ("OutputEnvelope".to_owned(), "CLI output envelope"),
+ ("next_actions".to_owned(), "CLI next-action rendering"),
+ ("exit_code".to_owned(), "CLI exit-code contract"),
+ ("docs/".to_owned(), "repository docs path"),
+ ("radroots store".to_owned(), "CLI command string"),
+ ("radroots sync".to_owned(), "CLI command string"),
+ ("radroots listing".to_owned(), "CLI command string"),
+ ("radroots trade".to_owned(), "CLI command string"),
];
for file in files {
let source = fs::read_to_string(&file).expect("read sdk source");
- for (needle, description) in forbidden {
+ for (needle, description) in forbidden.iter() {
assert!(
- !source.contains(needle),
+ !source.contains(needle.as_str()),
"SDK source contains {description} `{needle}` in {}",
file.display()
);
@@ -1808,10 +1808,13 @@ mod tests {
let source = fs::read_to_string(file).expect("read cli source");
let relative_path = relative_source_path(manifest_dir, file.as_path());
match production_source_without_tests(&relative_path, &source) {
- Ok(production_source) => production_source
- .contains("allow(dead_code)")
- .then(|| vec![format!("{relative_path}: production dead-code suppression")])
- .unwrap_or_default(),
+ Ok(production_source) => {
+ if production_source.contains("allow(dead_code)") {
+ vec![format!("{relative_path}: production dead-code suppression")]
+ } else {
+ Vec::new()
+ }
+ }
Err(error) => vec![error],
}
})
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -313,11 +313,11 @@ fn cli_write_kinds() -> [CliWriteKind; 9] {
event_kind: KIND_LISTING,
},
CliWriteKind {
- command: "listing.archive",
+ command: "listing.withdraw",
event_kind: KIND_LISTING,
},
CliWriteKind {
- command: "trade.submit",
+ command: "trade.request",
event_kind: KIND_ORDER_REQUEST,
},
CliWriteKind {
@@ -606,21 +606,21 @@ mod tests {
"farm.publish",
"listing.publish",
"listing.update",
- "listing.archive",
- "trade.submit",
+ "listing.withdraw",
+ "trade.request",
"trade.accept",
"trade.decline",
"trade.cancel",
]
);
- assert!(!commands.contains(&"signer.status.get"));
+ assert!(!commands.contains(&"signer.status"));
}
#[test]
fn order_submit_readiness_uses_active_order_request_kind() {
let write_kind = cli_write_kinds()
.into_iter()
- .find(|kind| kind.command == "trade.submit")
+ .find(|kind| kind.command == "trade.request")
.expect("order submit readiness");
assert_eq!(write_kind.event_kind, KIND_ORDER_REQUEST);
@@ -669,7 +669,7 @@ mod tests {
for (command, event_kind) in [
("farm.publish", KIND_FARM),
("listing.publish", KIND_LISTING),
- ("trade.submit", KIND_ORDER_REQUEST),
+ ("trade.request", KIND_ORDER_REQUEST),
] {
let entry = readiness
.iter()
diff --git a/src/runtime/store.rs b/src/runtime/store.rs
@@ -126,7 +126,7 @@ fn derived_projection_status(
pending_count: 0,
},
reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
+ actions: vec!["radroots store inspect".to_owned()],
});
}
@@ -187,7 +187,7 @@ pub fn backup_preflight(
reason: Some(
"dry run requested; SDK canonical backup directory was not written".to_owned(),
),
- actions: vec!["radroots store backup create".to_owned()],
+ actions: vec!["radroots store backup".to_owned()],
})
}
@@ -226,7 +226,7 @@ pub fn export(
export_version: String::new(),
schema_hash: String::new(),
reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
+ actions: vec!["radroots store inspect".to_owned()],
});
}
@@ -398,7 +398,7 @@ fn sdk_status_reason(state: &str) -> Option<String> {
fn sdk_status_actions(state: &str) -> Vec<String> {
match state {
"ready" => Vec::new(),
- _ => vec!["radroots store status get".to_owned()],
+ _ => vec!["radroots store inspect".to_owned()],
}
}
@@ -477,7 +477,7 @@ fn ensure_safe_sdk_backup_destination(
sdk_private_path.as_path(),
sdk_studio_path.as_path(),
];
- if forbidden_paths.iter().any(|forbidden| output == *forbidden) {
+ if forbidden_paths.contains(&output) {
return Err(RuntimeError::Config(format!(
"backup destination {} would overwrite canonical or derived projection store data",
output.display()
@@ -507,10 +507,7 @@ fn ensure_safe_sdk_restore_destination(
sdk_private_path.as_path(),
sdk_studio_path.as_path(),
];
- if forbidden_paths
- .iter()
- .any(|forbidden| destination == *forbidden)
- {
+ if forbidden_paths.contains(&destination) {
return Err(RuntimeError::Config(format!(
"restore destination {} would overwrite canonical runtime roots or store files",
destination.display()
@@ -529,9 +526,18 @@ fn ensure_safe_sdk_restore_destination(
}
fn sdk_backup_view(receipt: BackupReceipt) -> Result<LocalBackupView, CliSdkAdapterError> {
- let event_store_file = receipt.runtime_path.as_ref().map(display_path);
- let outbox_file = receipt.runtime_path.as_ref().map(display_path);
- let manifest_file = receipt.manifest_path.as_ref().map(display_path);
+ let event_store_file = receipt
+ .runtime_path
+ .as_ref()
+ .map(|path| display_path(path.as_path()));
+ let outbox_file = receipt
+ .runtime_path
+ .as_ref()
+ .map(|path| display_path(path.as_path()));
+ let manifest_file = receipt
+ .manifest_path
+ .as_ref()
+ .map(|path| display_path(path.as_path()));
let size_bytes = path_size(receipt.runtime_path.as_ref())?
+ path_size(receipt.private_path.as_ref())?
+ path_size(receipt.studio_path.as_ref())?
@@ -571,7 +577,7 @@ fn sdk_restore_view(
destination: receipt
.destination
.as_ref()
- .map(display_path)
+ .map(|path| display_path(path.as_path()))
.unwrap_or_default(),
event_store_file: display_path(&receipt.runtime_path),
outbox_file: display_path(&receipt.runtime_path),
@@ -591,7 +597,7 @@ fn sdk_restore_view(
None
},
actions: if dry_run {
- vec!["radroots store backup restore <backup-dir>".to_owned()]
+ vec!["radroots store restore <backup-dir>".to_owned()]
} else {
Vec::new()
},
@@ -659,7 +665,7 @@ fn path_size(path: Option<&PathBuf>) -> Result<u64, RuntimeError> {
.ok_or_else(|| RuntimeError::Config("SDK backup did not report all file paths".to_owned()))
}
-fn display_path(path: &PathBuf) -> String {
+fn display_path(path: &Path) -> String {
path.display().to_string()
}
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -42,7 +42,7 @@ use crate::runtime::sdk::{
};
use crate::view::runtime::{
SyncActionView, SyncFreshnessView, SyncQueueView, SyncRunFreshnessView, SyncStatusView,
- SyncTransportStatusView, SyncTransportTargetView, SyncWatchFrameView, SyncWatchView,
+ SyncTransportStatusInspectView, SyncTransportTargetView, SyncWatchFrameView, SyncWatchView,
TransportOperationCapabilitiesView, TransportTargetFailureView,
};
@@ -50,11 +50,11 @@ const SYNC_SOURCE: &str = "local replica · local first";
const SDK_SYNC_SOURCE: &str = "SDK canonical event store and outbox";
const SDK_PUSH_SOURCE: &str = "SDK outbox push";
const RELAY_PULL_SETUP_ACTION: &str =
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com";
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com";
const SYNC_PULL_ACTION: &str = "radroots sync pull";
const SYNC_PUSH_ACTION: &str = "radroots sync push";
-const SYNC_READY_ACTION: &str = "radroots market product search eggs";
-const MARKET_READY_ACTION: &str = "radroots market product search eggs";
+const SYNC_READY_ACTION: &str = "radroots market search eggs";
+const MARKET_READY_ACTION: &str = "radroots market search eggs";
const INGEST_SOURCE: &str = "shared Nostr transport fetch · local replica ingest";
const RELAY_FETCH_LIMIT: usize = 1_000;
const RELAY_FETCH_MAX_PAGES: usize = 5;
@@ -92,7 +92,7 @@ struct SyncSnapshot {
replica_store: String,
configured_transport_target_count: usize,
configured_transport_targets: Vec<SyncTransportTargetView>,
- transport_statuses: Vec<SyncTransportStatusView>,
+ transport_statuses: Vec<SyncTransportStatusInspectView>,
publish_policy: String,
freshness: SyncFreshnessView,
queue: SyncQueueView,
@@ -103,7 +103,7 @@ struct SyncSnapshot {
struct SyncTransportMetadata {
configured_transport_target_count: usize,
configured_transport_targets: Vec<SyncTransportTargetView>,
- transport_statuses: Vec<SyncTransportStatusView>,
+ transport_statuses: Vec<SyncTransportStatusInspectView>,
}
#[derive(Debug, Clone)]
@@ -176,7 +176,7 @@ where
RadrootsNostrFilter,
) -> Result<RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError>,
{
- relay_ingest(config, RelayIngestScope::MarketRefresh, fetcher)
+ relay_ingest(config, RelayIngestScope::MarketPull, fetcher)
}
fn shared_relay_transport_fetch_windowed(
@@ -531,6 +531,10 @@ fn sdk_push_view(
)
}
+#[expect(
+ clippy::too_many_arguments,
+ reason = "sync push view construction mirrors the V1 output contract fields"
+)]
fn sdk_push_action_view(
config: &RuntimeConfig,
state: &str,
@@ -538,7 +542,7 @@ fn sdk_push_action_view(
freshness: SyncFreshnessView,
configured_transport_target_count: usize,
configured_transport_targets: Vec<SyncTransportTargetView>,
- transport_statuses: Vec<SyncTransportStatusView>,
+ transport_statuses: Vec<SyncTransportStatusInspectView>,
target_transport_endpoints: Vec<String>,
attempted_transport_endpoints: Vec<String>,
accepted_transport_endpoints: Vec<String>,
@@ -595,7 +599,7 @@ fn sdk_transport_targets(receipt: &SyncStatusReceipt) -> Vec<SyncTransportTarget
.collect()
}
-fn sdk_transport_statuses(receipt: &SyncStatusReceipt) -> Vec<SyncTransportStatusView> {
+fn sdk_transport_statuses(receipt: &SyncStatusReceipt) -> Vec<SyncTransportStatusInspectView> {
receipt
.transport_profile
.transport_statuses
@@ -606,8 +610,8 @@ fn sdk_transport_statuses(receipt: &SyncStatusReceipt) -> Vec<SyncTransportStatu
fn sdk_transport_status_view(
status: &radroots_sdk::SyncTransportStatusSummary,
-) -> SyncTransportStatusView {
- SyncTransportStatusView {
+) -> SyncTransportStatusInspectView {
+ SyncTransportStatusInspectView {
transport: status.transport.clone(),
profile_id: status.profile_id.clone(),
endpoint_uri: status.endpoint_uri.clone(),
@@ -638,15 +642,15 @@ fn sdk_sync_status_actions(receipt: &SyncStatusReceipt) -> Vec<String> {
fn sdk_sync_push_actions(state: &str, retryable: bool) -> Vec<String> {
match state {
"published" | "ready" | "deferred_until_implemented" => {
- vec!["radroots sync status get".to_owned()]
+ vec!["radroots sync status".to_owned()]
}
"dry_run" | "partial" | "unavailable" if retryable => {
vec![
SYNC_PUSH_ACTION.to_owned(),
- "radroots sync status get".to_owned(),
+ "radroots sync status".to_owned(),
]
}
- _ => vec!["radroots sync status get".to_owned()],
+ _ => vec!["radroots sync status".to_owned()],
}
}
@@ -678,14 +682,12 @@ fn sdk_push_state(receipt: &PushOutboxReceipt, failed_count: usize) -> &'static
fn sdk_push_reported_deferred_state(receipt: &PushOutboxReceipt) -> Option<&'static str> {
let mut deferred = false;
for event in &receipt.events {
- match event.final_state {
- PushOutboxEventState::DeferredUntilImplemented => deferred = true,
- _ => {}
+ if event.final_state == PushOutboxEventState::DeferredUntilImplemented {
+ deferred = true;
}
for target in &event.targets {
- match target.outcome_kind {
- PushOutboxTargetOutcomeKind::DeferredUntilImplemented => deferred = true,
- _ => {}
+ if target.outcome_kind == PushOutboxTargetOutcomeKind::DeferredUntilImplemented {
+ deferred = true;
}
}
}
@@ -923,7 +925,7 @@ fn sync_transport_target_view(
})
}
-fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusView> {
+fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusInspectView> {
let profile_id = config.transport.profile.as_str();
let nostr_targets_configured = !config.transport.nostr_relay_urls.is_empty();
match config.transport.profile {
@@ -987,8 +989,8 @@ fn reticulum_transport_status(profile_id: &str) -> RadrootsTransportStatus {
.with_availability(RadrootsTransportCapabilityAvailability::Unavailable)
}
-fn sync_transport_status_view(status: RadrootsTransportStatus) -> SyncTransportStatusView {
- SyncTransportStatusView {
+fn sync_transport_status_view(status: RadrootsTransportStatus) -> SyncTransportStatusInspectView {
+ SyncTransportStatusInspectView {
transport: status.kind.canonical_label(),
profile_id: status.profile_id,
endpoint_uri: status.endpoint_uri,
@@ -1044,7 +1046,7 @@ fn inspect_sync(config: &RuntimeConfig) -> Result<SyncSnapshot, RuntimeError> {
freshness: missing_freshness(),
queue: derived_projection_sync_queue(0, 0),
reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
+ actions: vec!["radroots store inspect".to_owned()],
});
}
@@ -1072,7 +1074,7 @@ fn inspect_sync(config: &RuntimeConfig) -> Result<SyncSnapshot, RuntimeError> {
if transport_metadata.configured_transport_target_count == 0 {
RELAY_PULL_SETUP_ACTION.to_owned()
} else {
- "radroots transport profile get".to_owned()
+ "radroots transport config inspect".to_owned()
},
);
return Ok(SyncSnapshot {
@@ -1584,35 +1586,35 @@ fn relay_failure_reason(failed_transport_targets: &[TransportTargetFailureView])
#[derive(Debug, Clone, Copy)]
pub(crate) enum RelayIngestScope {
SyncPull,
- MarketRefresh,
+ MarketPull,
}
impl RelayIngestScope {
fn id(self) -> &'static str {
match self {
Self::SyncPull => "sync_pull",
- Self::MarketRefresh => "market_refresh",
+ Self::MarketPull => "market_refresh",
}
}
fn display(self) -> &'static str {
match self {
Self::SyncPull => "sync pull",
- Self::MarketRefresh => "market refresh",
+ Self::MarketPull => "market refresh",
}
}
fn stale_after_seconds(self) -> u64 {
match self {
Self::SyncPull => SYNC_PULL_FRESHNESS_STALE_AFTER_SECONDS,
- Self::MarketRefresh => MARKET_FRESHNESS_STALE_AFTER_SECONDS,
+ Self::MarketPull => MARKET_FRESHNESS_STALE_AFTER_SECONDS,
}
}
fn kinds(self) -> &'static [u32] {
match self {
Self::SyncPull => SYNC_PULL_KINDS,
- Self::MarketRefresh => MARKET_REFRESH_KINDS,
+ Self::MarketPull => MARKET_REFRESH_KINDS,
}
}
@@ -1630,7 +1632,7 @@ impl RelayIngestScope {
fn ready_action(self) -> &'static str {
match self {
Self::SyncPull => SYNC_READY_ACTION,
- Self::MarketRefresh => MARKET_READY_ACTION,
+ Self::MarketPull => MARKET_READY_ACTION,
}
}
@@ -1846,7 +1848,7 @@ mod tests {
assert_eq!(
view.actions,
vec![
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
]
);
}
@@ -1880,7 +1882,7 @@ mod tests {
assert_eq!(
view.actions,
vec![
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
]
);
}
@@ -1967,7 +1969,7 @@ mod tests {
assert!(!view.transport_statuses[1].capabilities.deliver);
assert!(!view.transport_statuses[1].capabilities.fetch);
assert!(view.target_transport_endpoints.is_empty());
- assert_eq!(view.actions, vec!["radroots transport profile get"]);
+ assert_eq!(view.actions, vec!["radroots transport config inspect"]);
}
#[test]
@@ -2233,7 +2235,7 @@ mod tests {
);
assert_eq!(
view.actions,
- vec!["radroots sync push", "radroots sync status get"]
+ vec!["radroots sync push", "radroots sync status"]
);
assert!(view.publish_plan.is_none());
}
@@ -2270,7 +2272,7 @@ mod tests {
view.reason.as_deref(),
Some("SDK outbox had no ready signed events to push")
);
- assert_eq!(view.actions, vec!["radroots sync status get"]);
+ assert_eq!(view.actions, vec!["radroots sync status"]);
}
#[test]
@@ -2341,7 +2343,7 @@ mod tests {
view.failed_transport_targets[0].reason,
RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
);
- assert_eq!(view.actions, vec!["radroots sync status get"]);
+ assert_eq!(view.actions, vec!["radroots sync status"]);
}
}
@@ -2432,10 +2434,14 @@ mod tests {
);
assert_eq!(
view.actions,
- vec!["radroots sync push", "radroots sync status get"]
+ vec!["radroots sync push", "radroots sync status"]
);
}
+ #[expect(
+ clippy::too_many_arguments,
+ reason = "test fixture construction sets every storage status field explicitly"
+ )]
fn sdk_status_receipt(
total_events: i64,
outbox_total_events: i64,
@@ -2757,7 +2763,7 @@ mod tests {
let _ = market_refresh_with_fetcher(&config, fake_fetcher(vec![listing_event(&seller)]))
.expect("market refresh");
- let relays = relay_provenance_relays_for_scope(&config, RelayIngestScope::MarketRefresh)
+ let relays = relay_provenance_relays_for_scope(&config, RelayIngestScope::MarketPull)
.expect("relay provenance");
assert_eq!(
@@ -2783,7 +2789,7 @@ mod tests {
let run = view.freshness.run.as_ref().expect("run freshness");
assert_eq!(run.scope, "market_refresh");
assert_eq!(run.last_state, "success");
- assert_eq!(run.relay_set_current, true);
+ assert!(run.relay_set_current);
assert_eq!(run.fetched_count, Some(1));
assert_eq!(run.ingested_count, Some(1));
}
@@ -2884,7 +2890,7 @@ mod tests {
assert_eq!(freshness.state, "relay_set_changed");
let run = freshness.run.as_ref().expect("run freshness");
assert_eq!(run.scope, "sync_pull");
- assert_eq!(run.relay_set_current, false);
+ assert!(!run.relay_set_current);
}
#[test]
diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs
@@ -17,9 +17,9 @@ use crate::runtime::RuntimeError;
use crate::runtime::config::{RuntimeConfig, TransportProfileKind};
use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy};
use crate::view::runtime::{
- TransportOperationCapabilitiesView, TransportOutboxPushView, TransportOutboxStatusView,
+ TransportDeliveryInspectView, TransportDeliveryRetryView, TransportOperationCapabilitiesView,
TransportProfileSummaryView, TransportProfileView, TransportRuntimeStatusView,
- TransportStatusView,
+ TransportStatusInspectView,
};
const TRANSPORT_SOURCE: &str = "transport profile config";
@@ -112,10 +112,10 @@ pub fn set_profile(
))
}
-pub fn status(config: &RuntimeConfig) -> TransportStatusView {
+pub fn status(config: &RuntimeConfig) -> TransportStatusInspectView {
let profile = active_profile_view(config);
- TransportStatusView {
+ TransportStatusInspectView {
state: "ready".to_owned(),
source: TRANSPORT_SOURCE.to_owned(),
active_profile: profile.summary(),
@@ -125,7 +125,7 @@ pub fn status(config: &RuntimeConfig) -> TransportStatusView {
pub fn outbox_status(
config: &RuntimeConfig,
-) -> Result<TransportOutboxStatusView, CliSdkAdapterError> {
+) -> Result<TransportDeliveryInspectView, CliSdkAdapterError> {
let profile = active_profile_view(config);
let session = if profile.profile_delivery_usable {
CliSdkSession::connect(config)?
@@ -138,7 +138,7 @@ pub fn outbox_status(
} else {
profile.configured_state.clone()
};
- Ok(TransportOutboxStatusView {
+ Ok(TransportDeliveryInspectView {
state,
source: "SDK transport outbox".to_owned(),
transport_profile: profile.profile_id,
@@ -155,10 +155,12 @@ pub fn outbox_status(
})
}
-pub fn outbox_push(config: &RuntimeConfig) -> Result<TransportOutboxPushView, CliSdkAdapterError> {
+pub fn outbox_push(
+ config: &RuntimeConfig,
+) -> Result<TransportDeliveryRetryView, CliSdkAdapterError> {
if config.output.dry_run {
let status = outbox_status(config)?;
- return Ok(TransportOutboxPushView {
+ return Ok(TransportDeliveryRetryView {
state: "dry_run".to_owned(),
source: "SDK transport outbox".to_owned(),
attempted_events: 0,
@@ -181,7 +183,7 @@ pub fn outbox_push(config: &RuntimeConfig) -> Result<TransportOutboxPushView, Cl
.count();
let failed_count = receipt.retryable_events + receipt.terminal_events;
let state = transport_outbox_push_state(&receipt, failed_count).to_owned();
- Ok(TransportOutboxPushView {
+ Ok(TransportDeliveryRetryView {
state,
source: "SDK transport outbox".to_owned(),
attempted_events: receipt.attempted_events,
@@ -212,14 +214,12 @@ fn transport_outbox_push_state(receipt: &PushOutboxReceipt, failed_count: usize)
fn transport_outbox_reported_deferred_state(receipt: &PushOutboxReceipt) -> Option<&'static str> {
let mut deferred = false;
for event in &receipt.events {
- match event.final_state {
- PushOutboxEventState::DeferredUntilImplemented => deferred = true,
- _ => {}
+ if event.final_state == PushOutboxEventState::DeferredUntilImplemented {
+ deferred = true;
}
for target in &event.targets {
- match target.outcome_kind {
- PushOutboxTargetOutcomeKind::DeferredUntilImplemented => deferred = true,
- _ => {}
+ if target.outcome_kind == PushOutboxTargetOutcomeKind::DeferredUntilImplemented {
+ deferred = true;
}
}
}
@@ -456,22 +456,22 @@ fn profile_actions(profile_id: &str, profile_delivery_usable: bool) -> Vec<Strin
}
match profile_id {
"reticulum" => vec![
- "radroots mesh status".to_owned(),
- "radroots transport profile get".to_owned(),
+ "radroots transport status inspect".to_owned(),
+ "radroots transport config inspect".to_owned(),
],
"nostr" => {
vec![
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
.to_owned(),
]
}
"multi_target" => {
vec![
- "radroots transport profile set --kind multi-target --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind multi-target --nostr-relay wss://relay.example.com"
.to_owned(),
]
}
- _ => vec!["radroots transport profile get".to_owned()],
+ _ => vec!["radroots transport config inspect".to_owned()],
}
}
diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs
@@ -634,7 +634,7 @@ fn sdk_error_parts(error: RadrootsSdkError) -> ValidationReceiptSdkErrorParts {
};
let actions = if error.code() == "empty_transport_targets" {
vec![
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
.to_owned(),
]
} else {
@@ -1258,11 +1258,13 @@ mod tests {
proof_state_from_sp1_error, proof_state_is_invalid, proof_state_is_verification_success,
proof_verification_view_for_receipt, validation_receipt_invalid_reason_code,
};
+ use radroots_event::ids::{RadrootsAddressableCoordinate, RadrootsPublicKey};
use radroots_trade::validation_receipt::{
RadrootsTradeValidationReceipt, RadrootsValidationReceiptError,
RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem,
RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement,
RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, VALIDATION_RECEIPT_VERSION,
+ validator_set_address,
};
use radroots_trade_sp1_host::RadrootsSp1TradeHostError;
@@ -1305,12 +1307,24 @@ mod tests {
.to_owned(),
target_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
.to_owned(),
+ validator_set_addr: validator_set_addr(),
+ validator_set_event_id:
+ "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_owned(),
statement_type: RadrootsValidationReceiptType::TradeTransition,
},
version: VALIDATION_RECEIPT_VERSION,
}
}
+ fn validator_set_addr() -> RadrootsAddressableCoordinate {
+ let author = RadrootsPublicKey::parse(
+ "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
+ )
+ .expect("validator set author");
+ validator_set_address(&author, "018f3d99-7d35-7c0c-8a0f-7f3b645abcde")
+ .expect("validator set address")
+ }
+
fn deterministic_receipt() -> RadrootsTradeValidationReceipt {
receipt_with_proof(RadrootsValidationReceiptProof {
inline_proof_base64: None,
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -1171,6 +1171,10 @@ impl MarketReadinessView {
}
}
+ #[expect(
+ clippy::too_many_arguments,
+ reason = "market candidate construction maps projection fields explicitly"
+ )]
pub fn from_market_projection(
listing_addr: Option<&str>,
primary_bin_id: Option<&str>,
@@ -3107,7 +3111,7 @@ pub struct TransportProfileView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct TransportStatusView {
+pub struct TransportStatusInspectView {
pub state: String,
pub source: String,
pub active_profile: TransportProfileSummaryView,
@@ -3146,7 +3150,7 @@ pub struct TransportOperationCapabilitiesView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct TransportOutboxStatusView {
+pub struct TransportDeliveryInspectView {
pub state: String,
pub source: String,
pub transport_profile: String,
@@ -3166,7 +3170,7 @@ pub struct TransportOutboxStatusView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct TransportOutboxPushView {
+pub struct TransportDeliveryRetryView {
pub state: String,
pub source: String,
pub attempted_events: usize,
@@ -3303,7 +3307,7 @@ pub struct SyncStatusView {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub configured_transport_targets: Vec<SyncTransportTargetView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub transport_statuses: Vec<SyncTransportStatusView>,
+ pub transport_statuses: Vec<SyncTransportStatusInspectView>,
pub publish_policy: String,
pub freshness: SyncFreshnessView,
pub queue: SyncQueueView,
@@ -3335,7 +3339,7 @@ pub struct SyncActionView {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub configured_transport_targets: Vec<SyncTransportTargetView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub transport_statuses: Vec<SyncTransportStatusView>,
+ pub transport_statuses: Vec<SyncTransportStatusInspectView>,
pub publish_policy: String,
pub freshness: SyncFreshnessView,
pub queue: SyncQueueView,
@@ -3383,7 +3387,7 @@ pub struct SyncTransportTargetView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct SyncTransportStatusView {
+pub struct SyncTransportStatusInspectView {
pub transport: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -1,2773 +0,0 @@
-mod support;
-
-use std::fs;
-use std::path::Path;
-
-use serde_json::Value;
-use support::{
- RadrootsCliSandbox, assert_contains, assert_no_daemon_runtime_reference,
- assert_no_removed_command_reference, create_listing_draft, identity_public, identity_secret,
- json_from_stdout, make_listing_publishable, seed_orderable_listing, shell_single_quoted,
- toml_string, write_public_identity_profile, write_secret_identity_profile,
-};
-
-const LISTING_ADDR: &str =
- "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
-#[test]
-fn local_signer_status_reports_unconfigured_without_account() {
- let sandbox = RadrootsCliSandbox::new();
-
- let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
-
- assert_eq!(value["schema_version"], "radroots.cli.output.v1");
- assert_eq!(value["operation_id"], "signer.status.get");
- assert_eq!(value["kind"], "signer.status.get");
- assert_eq!(value["result"]["mode"], "local");
- assert_eq!(value["result"]["state"], "unconfigured");
- assert_eq!(
- value["result"]["signer_account_id"],
- serde_json::Value::Null
- );
- assert_eq!(value["result"]["binding"]["state"], "disabled");
- assert_eq!(value["result"]["local"], serde_json::Value::Null);
- assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
-}
-
-#[test]
-fn local_signer_status_reports_ready_after_account_create() {
- let sandbox = RadrootsCliSandbox::new();
-
- let created = sandbox.json_success(&["--format", "json", "account", "create"]);
- assert_eq!(created["operation_id"], "account.create");
- assert_eq!(created["result"]["state"], "created");
- assert_eq!(created["result"]["account"]["signer"], "local");
- assert_eq!(created["result"]["account"]["custody"], "secret_backed");
- assert_eq!(created["result"]["account"]["write_capable"], true);
- assert_eq!(created["result"]["account"]["is_default"], true);
- let account_id = created["result"]["account"]["id"]
- .as_str()
- .expect("created account id");
-
- let status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
-
- assert_eq!(status["operation_id"], "signer.status.get");
- assert_eq!(status["result"]["mode"], "local");
- assert_eq!(status["result"]["state"], "ready");
- assert_eq!(status["result"]["signer_account_id"], account_id);
- assert_eq!(status["result"]["local"]["account_id"], account_id);
- assert_eq!(status["result"]["local"]["availability"], "secret_backed");
- assert_eq!(status["result"]["local"]["secret_backed"], true);
- assert_eq!(status["result"]["local"]["backend"], "encrypted_file");
- assert_eq!(status["result"]["binding"]["state"], "disabled");
-}
-
-#[test]
-fn local_account_selection_and_invocation_override_resolve_signer_actor() {
- let sandbox = RadrootsCliSandbox::new();
-
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
- assert_eq!(first["result"]["account"]["is_default"], true);
- assert_eq!(second["result"]["account"]["is_default"], false);
-
- let default_status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- assert_eq!(default_status["result"]["state"], "ready");
- assert_eq!(
- default_status["result"]["signer_account_id"],
- first_account_id
- );
- assert_eq!(
- default_status["result"]["account_resolution"]["source"],
- "default_account"
- );
-
- let override_status = sandbox.json_success(&[
- "--format",
- "json",
- "--account-id",
- second_account_id,
- "signer",
- "status",
- "get",
- ]);
- assert_eq!(override_status["actor"]["account_id"], second_account_id);
- assert_eq!(override_status["actor"]["role"], "account");
- assert_eq!(
- override_status["result"]["signer_account_id"],
- second_account_id
- );
- assert_eq!(
- override_status["result"]["account_resolution"]["source"],
- "invocation_override"
- );
- assert_eq!(
- override_status["result"]["account_resolution"]["default_account"]["id"],
- first_account_id
- );
-
- let selected = sandbox.json_success(&[
- "--format",
- "json",
- "account",
- "selection",
- "update",
- second_account_id,
- ]);
- assert_eq!(selected["operation_id"], "account.selection.update");
- assert_eq!(selected["result"]["state"], "default");
- assert_eq!(selected["result"]["account"]["id"], second_account_id);
-
- let selected_status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- assert_eq!(
- selected_status["result"]["signer_account_id"],
- second_account_id
- );
- assert_eq!(
- selected_status["result"]["account_resolution"]["source"],
- "default_account"
- );
-
- let selected_get =
- sandbox.json_success(&["--format", "json", "account", "get", first_account_id]);
- assert_eq!(selected_get["operation_id"], "account.get");
- assert_eq!(
- selected_get["result"]["account_resolution"]["source"],
- "invocation_override"
- );
- assert_eq!(
- selected_get["result"]["account_resolution"]["resolved_account"]["id"],
- first_account_id
- );
-}
-
-#[test]
-fn account_import_dry_run_validates_profile_without_mutating_store() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(21);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "dry-run-import", &public_identity);
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
-
- assert_eq!(value["operation_id"], "account.import");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(
- value["result"]["account"]["id"],
- public_identity.id.as_str()
- );
- assert_eq!(value["result"]["account"]["signer"], "watch_only");
- assert_eq!(value["result"]["account"]["custody"], "watch_only");
- assert_eq!(value["result"]["account"]["write_capable"], false);
- assert_eq!(value["result"]["account"]["is_default"], true);
-
- let list = sandbox.json_success(&["--format", "json", "account", "list"]);
- assert_eq!(list["result"]["count"], 0);
-}
-
-#[test]
-fn account_import_dry_run_validates_missing_profile_file() {
- let sandbox = RadrootsCliSandbox::new();
- let missing = sandbox.root().join("missing-account.json");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "import",
- missing.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "account.import");
- assert_eq!(value["errors"][0]["code"], "not_found");
- assert_eq!(value["errors"][0]["exit_code"], 4);
-}
-
-#[test]
-fn account_attach_secret_dry_run_validates_without_mutating_store() {
- let sandbox = RadrootsCliSandbox::new();
- let default_account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let default_account_id = default_account["result"]["account"]["id"]
- .as_str()
- .expect("default account id");
- let identity = identity_secret(31);
- let public_identity = identity.to_public();
- let public_identity_file =
- write_public_identity_profile(&sandbox, "attach-dry-public", &public_identity);
- let secret_identity_file =
- write_secret_identity_profile(&sandbox, "attach-dry-secret", &identity);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let watch_account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("watch account id");
- assert_eq!(imported["result"]["account"]["signer"], "watch_only");
- assert_eq!(imported["result"]["account"]["custody"], "watch_only");
- assert_eq!(imported["result"]["account"]["write_capable"], false);
- assert_eq!(imported["result"]["account"]["is_default"], false);
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "attach-secret",
- watch_account_id,
- secret_identity_file.to_string_lossy().as_ref(),
- "--default",
- ]);
-
- assert_eq!(value["operation_id"], "account.attach_secret");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["default"], true);
- assert_eq!(value["result"]["account"]["id"], watch_account_id);
- assert_eq!(value["result"]["account"]["signer"], "local");
- assert_eq!(value["result"]["account"]["custody"], "secret_backed");
- assert_eq!(value["result"]["account"]["write_capable"], true);
- assert_eq!(value["result"]["account"]["is_default"], true);
-
- let watch_get = sandbox.json_success(&["--format", "json", "account", "get", watch_account_id]);
- assert_eq!(
- watch_get["result"]["account_resolution"]["resolved_account"]["signer"],
- "watch_only"
- );
- assert_eq!(
- watch_get["result"]["account_resolution"]["resolved_account"]["custody"],
- "watch_only"
- );
- assert_eq!(
- watch_get["result"]["account_resolution"]["resolved_account"]["write_capable"],
- false
- );
- let selected = sandbox.json_success(&["--format", "json", "account", "selection", "get"]);
- assert_eq!(
- selected["result"]["account_resolution"]["resolved_account"]["id"],
- default_account_id
- );
-}
-
-#[test]
-fn account_attach_secret_attaches_matching_secret_and_can_make_default() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let identity = identity_secret(32);
- let public_identity = identity.to_public();
- let public_identity_file =
- write_public_identity_profile(&sandbox, "attach-public", &public_identity);
- let secret_identity_file = write_secret_identity_profile(&sandbox, "attach-secret", &identity);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let watch_account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("watch account id");
-
- let attached = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "attach-secret",
- watch_account_id,
- secret_identity_file.to_string_lossy().as_ref(),
- "--default",
- ]);
-
- assert_eq!(attached["operation_id"], "account.attach_secret");
- assert_eq!(attached["result"]["state"], "secret_attached");
- assert_eq!(attached["result"]["account"]["id"], watch_account_id);
- assert_eq!(attached["result"]["account"]["signer"], "local");
- assert_eq!(attached["result"]["account"]["custody"], "secret_backed");
- assert_eq!(attached["result"]["account"]["write_capable"], true);
- assert_eq!(attached["result"]["account"]["is_default"], true);
-
- let status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- assert_eq!(status["result"]["state"], "ready");
- assert_eq!(status["result"]["signer_account_id"], watch_account_id);
- assert_eq!(status["result"]["local"]["availability"], "secret_backed");
-}
-
-#[test]
-fn account_attach_secret_requires_approval_before_writing_secret() {
- let sandbox = RadrootsCliSandbox::new();
- let identity = identity_secret(33);
- let public_identity = identity.to_public();
- let public_identity_file =
- write_public_identity_profile(&sandbox, "attach-approval-public", &public_identity);
- let secret_identity_file =
- write_secret_identity_profile(&sandbox, "attach-approval-secret", &identity);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("account id");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "account",
- "attach-secret",
- account_id,
- secret_identity_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "account.attach_secret");
- assert_eq!(value["errors"][0]["code"], "approval_required");
- assert_eq!(value["errors"][0]["exit_code"], 6);
- let get = sandbox.json_success(&["--format", "json", "account", "get", account_id]);
- assert_eq!(
- get["result"]["account_resolution"]["resolved_account"]["signer"],
- "watch_only"
- );
- assert_eq!(
- get["result"]["account_resolution"]["resolved_account"]["custody"],
- "watch_only"
- );
- assert_eq!(
- get["result"]["account_resolution"]["resolved_account"]["write_capable"],
- false
- );
-}
-
-#[test]
-fn account_attach_secret_reports_structured_validation_failures() {
- let sandbox = RadrootsCliSandbox::new();
- let matching_identity = identity_secret(34);
- let public_identity = matching_identity.to_public();
- let public_identity_file =
- write_public_identity_profile(&sandbox, "attach-fail-public", &public_identity);
- let secret_identity_file =
- write_secret_identity_profile(&sandbox, "attach-fail-secret", &matching_identity);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("account id");
-
- let (missing_input_output, missing_input) =
- sandbox.json_output(&["--format", "json", "--dry-run", "account", "attach-secret"]);
- assert!(!missing_input_output.status.success());
- assert_eq!(missing_input["operation_id"], "account.attach_secret");
- assert_eq!(missing_input["errors"][0]["code"], "invalid_input");
-
- let (missing_account_output, missing_account) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "attach-secret",
- "missing-account",
- secret_identity_file.to_string_lossy().as_ref(),
- ]);
- assert!(!missing_account_output.status.success());
- assert_eq!(missing_account["errors"][0]["code"], "account_unresolved");
- assert_eq!(missing_account["errors"][0]["exit_code"], 5);
-
- let mismatched_identity = identity_secret(35);
- let mismatched_identity_file =
- write_secret_identity_profile(&sandbox, "attach-mismatch-secret", &mismatched_identity);
- let (mismatch_output, mismatch) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "attach-secret",
- account_id,
- mismatched_identity_file.to_string_lossy().as_ref(),
- ]);
- assert!(!mismatch_output.status.success());
- assert_eq!(mismatch["errors"][0]["code"], "account_mismatch");
- assert_eq!(mismatch["errors"][0]["exit_code"], 5);
-
- let invalid_identity_file = sandbox.root().join("attach-invalid-secret.json");
- fs::write(&invalid_identity_file, "{ invalid json").expect("write invalid identity");
- let (invalid_output, invalid) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "attach-secret",
- account_id,
- invalid_identity_file.to_string_lossy().as_ref(),
- ]);
- assert!(!invalid_output.status.success());
- assert_eq!(invalid["errors"][0]["code"], "validation_failed");
- assert_eq!(invalid["errors"][0]["exit_code"], 10);
-
- let mut unavailable_command = sandbox.command();
- unavailable_command
- .env("RADROOTS_CLI_ACCOUNT_SECRET_BACKEND", "host_vault")
- .env("RADROOTS_CLI_ACCOUNT_HOST_VAULT_AVAILABLE", "false")
- .args([
- "--format",
- "json",
- "--dry-run",
- "account",
- "attach-secret",
- account_id,
- secret_identity_file.to_string_lossy().as_ref(),
- ]);
- let unavailable_output = unavailable_command
- .output()
- .expect("run unavailable backend");
- let unavailable = json_from_stdout(&unavailable_output);
- assert!(!unavailable_output.status.success());
- assert_eq!(unavailable["errors"][0]["code"], "operation_unavailable");
- assert_eq!(unavailable["errors"][0]["exit_code"], 3);
-}
-
-#[test]
-fn account_remove_dry_run_validates_selector_without_mutating_store() {
- let sandbox = RadrootsCliSandbox::new();
- let created = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = created["result"]["account"]["id"]
- .as_str()
- .expect("account id");
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "remove",
- account_id,
- ]);
-
- assert_eq!(value["operation_id"], "account.remove");
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["removed_account"]["id"], account_id);
- assert_eq!(value["result"]["default_would_clear"], true);
- assert_eq!(value["result"]["remaining_account_count"], 0);
-
- let get = sandbox.json_success(&["--format", "json", "account", "get", account_id]);
- assert_eq!(get["result"]["state"], "ready");
- assert_eq!(
- get["result"]["account_resolution"]["resolved_account"]["id"],
- account_id
- );
-}
-
-#[test]
-fn account_remove_warns_when_farm_bound_seller_is_orphaned() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- let farm = create_test_farm(&sandbox);
- let farm_path = farm["result"]["config"]["path"]
- .as_str()
- .expect("farm path");
- let farm_before_remove = fs::read_to_string(farm_path).expect("farm before account remove");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
-
- let non_bound_removed = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- second_account_id,
- ]);
-
- assert_eq!(non_bound_removed["operation_id"], "account.remove");
- assert_eq!(non_bound_removed["result"]["state"], "removed");
- assert_eq!(
- non_bound_removed["result"]["removed_account"]["id"],
- second_account_id
- );
- assert!(non_bound_removed["result"].get("warnings").is_none());
- assert!(non_bound_removed["result"].get("actions").is_none());
- assert!(
- non_bound_removed["warnings"]
- .as_array()
- .expect("top-level warnings")
- .is_empty()
- );
- assert!(
- non_bound_removed["next_actions"]
- .as_array()
- .expect("next actions")
- .is_empty()
- );
- assert_eq!(
- fs::read_to_string(farm_path).expect("farm after non-bound remove"),
- farm_before_remove
- );
-
- let orphaned = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- first_account_id,
- ]);
-
- assert_eq!(orphaned["operation_id"], "account.remove");
- assert_eq!(orphaned["result"]["state"], "removed");
- assert_eq!(
- orphaned["result"]["removed_account"]["id"],
- first_account_id
- );
- assert_eq!(
- orphaned["result"]["warnings"][0]["code"],
- "farm_bound_seller_orphaned"
- );
- assert_eq!(
- orphaned["result"]["warnings"][0]["subject_account_id"],
- first_account_id
- );
- assert_eq!(
- orphaned["result"]["warnings"][0]["farm_config"]["scope"],
- "workspace"
- );
- assert_eq!(
- orphaned["result"]["warnings"][0]["farm_config"]["path"],
- farm_path
- );
- assert_eq!(
- orphaned["warnings"][0]["code"],
- "farm_bound_seller_orphaned"
- );
- assert_action_present(&orphaned, "radroots account import <path>");
- assert_action_present(&orphaned, "radroots --dry-run farm rebind <selector>");
- assert_action_present(
- &orphaned,
- "radroots --approval-token approve farm rebind <selector>",
- );
- assert_next_action_present(&orphaned, "radroots account import <path>");
- assert_next_action_present(&orphaned, "radroots --dry-run farm rebind <selector>");
- assert_next_action_present(
- &orphaned,
- "radroots --approval-token approve farm rebind <selector>",
- );
- assert_eq!(
- fs::read_to_string(farm_path).expect("farm after farm-bound remove"),
- farm_before_remove
- );
-}
-
-#[test]
-fn account_remove_farm_orphan_warning_renders_terminal() {
- let sandbox = RadrootsCliSandbox::new();
- let created = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = created["result"]["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
- create_test_farm(&sandbox);
-
- let output = sandbox
- .command()
- .args([
- "--approval-token",
- "approve",
- "account",
- "remove",
- account_id.as_str(),
- ])
- .output()
- .expect("terminal account remove");
-
- assert!(output.status.success());
- assert!(output.stderr.is_empty());
- let stdout = String::from_utf8(output.stdout).expect("terminal stdout");
- assert!(stdout.contains("Account removed"), "{stdout}");
- assert!(stdout.contains(account_id.as_str()), "{stdout}");
- assert!(
- stdout.contains("Warnings\n farm_bound_seller_orphaned:"),
- "{stdout}"
- );
- assert!(
- stdout.contains("Next\n radroots account import <path>"),
- "{stdout}"
- );
- assert!(
- stdout.contains("radroots --dry-run farm rebind <selector>"),
- "{stdout}"
- );
- assert!(
- stdout.contains("radroots --approval-token approve farm rebind <selector>"),
- "{stdout}"
- );
-}
-
-#[test]
-fn account_selection_update_dry_run_validates_selector_without_mutating_selection() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "selection",
- "update",
- second_account_id,
- ]);
-
- assert_eq!(value["operation_id"], "account.selection.update");
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["account"]["id"], second_account_id);
-
- let selected = sandbox.json_success(&["--format", "json", "account", "selection", "get"]);
- assert_eq!(
- selected["result"]["account_resolution"]["resolved_account"]["id"],
- first_account_id
- );
-}
-
-#[test]
-fn account_selection_update_dry_run_rejects_missing_selector() {
- let sandbox = RadrootsCliSandbox::new();
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "selection",
- "update",
- "missing-account",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "account.selection.update");
- assert_eq!(value["errors"][0]["code"], "account_unresolved");
- assert_eq!(value["errors"][0]["exit_code"], 5);
-}
-
-#[test]
-fn unresolved_account_override_returns_account_failure() {
- let sandbox = RadrootsCliSandbox::new();
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--account-id",
- "missing-account",
- "account",
- "get",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "account.get");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_unresolved");
- assert_eq!(value["errors"][0]["exit_code"], 5);
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_contains(&value["errors"][0]["message"], "account selector");
-}
-
-#[test]
-fn watch_only_import_reports_unconfigured_local_signer() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(11);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "watch-only", &public_identity);
-
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
-
- assert_eq!(imported["operation_id"], "account.import");
- assert_eq!(imported["result"]["state"], "imported");
- assert_eq!(
- imported["result"]["account"]["id"],
- public_identity.id.as_str()
- );
- assert_eq!(imported["result"]["account"]["signer"], "watch_only");
- assert_eq!(imported["result"]["account"]["custody"], "watch_only");
- assert_eq!(imported["result"]["account"]["write_capable"], false);
- assert_eq!(imported["result"]["account"]["is_default"], true);
-
- let status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
-
- assert_eq!(status["result"]["mode"], "local");
- assert_eq!(status["result"]["state"], "unconfigured");
- assert_eq!(
- status["result"]["signer_account_id"],
- public_identity.id.as_str()
- );
- assert_eq!(
- status["result"]["account_resolution"]["source"],
- "default_account"
- );
- assert_eq!(
- status["result"]["account_resolution"]["resolved_account"]["signer"],
- "watch_only"
- );
- assert_eq!(
- status["result"]["account_resolution"]["resolved_account"]["custody"],
- "watch_only"
- );
- assert_eq!(
- status["result"]["account_resolution"]["resolved_account"]["write_capable"],
- false
- );
- assert_eq!(
- status["result"]["local"]["account_id"],
- public_identity.id.as_str()
- );
- assert_eq!(status["result"]["local"]["availability"], "public_only");
- assert_eq!(status["result"]["local"]["secret_backed"], false);
- assert_contains(&status["result"]["reason"], "not secret-backed");
- assert!(
- status["result"]["write_kinds"]
- .as_array()
- .expect("write kinds")
- .iter()
- .all(|kind| kind["ready"] == false)
- );
-}
-
-#[test]
-fn myc_signer_status_reports_missing_binding() {
- let sandbox = RadrootsCliSandbox::new();
- let missing_myc = sandbox.root().join("bin/missing-myc");
- configure_myc_mode(&sandbox, &missing_myc);
-
- let (output, value) = sandbox.json_output(&["--format", "json", "signer", "status", "get"]);
-
- assert!(output.status.success());
- assert_eq!(value["operation_id"], "signer.status.get");
- assert!(value["errors"].as_array().expect("errors").is_empty());
- assert_eq!(value["result"]["mode"], "myc");
- assert_eq!(value["result"]["state"], "unconfigured");
- assert_eq!(value["result"]["binding"]["state"], "unconfigured");
- assert_eq!(value["result"]["myc"]["state"], "unconfigured");
- assert_eq!(value["result"]["myc"]["ready"], false);
- assert_contains(
- &value["result"]["reason"],
- "signer.remote_nip46 binding is missing",
- );
- assert_no_removed_command_reference(&value, &["signer", "status", "get"]);
-}
-
-#[test]
-fn myc_signer_status_fails_closed_when_managed_account_is_unresolved() {
- let sandbox = RadrootsCliSandbox::new();
- let missing_myc = sandbox.root().join("bin/missing-myc");
- let remote_signer = identity_public(91);
- sandbox.write_app_config(&format!(
- r#"[signer]
-backend = "myc"
-
-[myc]
-executable = "{}"
-
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "explicit_endpoint"
-target = "bunker://{}?relay=wss%3A%2F%2Frelay.example"
-managed_account_ref = "acct_missing"
-signer_session_ref = "session_missing"
-"#,
- toml_string(missing_myc.display().to_string().as_str()),
- remote_signer.public_key_hex,
- ));
-
- let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
-
- assert_eq!(value["result"]["mode"], "myc");
- assert_eq!(value["result"]["state"], "unconfigured");
- assert_eq!(value["result"]["binding"]["state"], "unconfigured");
- assert_eq!(value["result"]["myc"]["ready"], false);
- assert_contains(
- &value["result"]["reason"],
- "managed_account_ref `acct_missing` cannot be evaluated",
- );
- assert!(
- value["result"]["write_kinds"]
- .as_array()
- .expect("write kinds")
- .iter()
- .all(|kind| kind["ready"] == false)
- );
-}
-
-#[cfg(unix)]
-#[test]
-fn myc_signer_status_does_not_invoke_configured_executable() {
- let sandbox = RadrootsCliSandbox::new();
- let invoked = sandbox.root().join("myc-invoked.txt");
- let myc = sandbox.write_fake_myc(
- "myc-deferred",
- format!(
- "printf invoked > '{}'",
- shell_single_quoted(invoked.to_string_lossy().as_ref())
- )
- .as_str(),
- );
- configure_myc_mode(&sandbox, &myc);
-
- let (output, value) = sandbox.json_output(&["--format", "json", "signer", "status", "get"]);
-
- assert!(output.status.success());
- assert_eq!(value["operation_id"], "signer.status.get");
- assert!(value["errors"].as_array().expect("errors").is_empty());
- assert_eq!(value["result"]["state"], "unconfigured");
- assert_eq!(value["result"]["myc"]["ready"], false);
- assert!(!invoked.exists(), "target CLI must not execute MYC");
-}
-
-#[test]
-fn myc_mode_allows_read_inspection_commands() {
- let sandbox = RadrootsCliSandbox::new();
- let missing_myc = sandbox.root().join("bin/missing-myc");
- configure_myc_mode(&sandbox, &missing_myc);
-
- for args in [
- &["--format", "json", "workspace", "get"][..],
- &["--format", "json", "config", "get"][..],
- &["--format", "json", "account", "list"][..],
- &["--format", "json", "transport", "profile", "get"][..],
- ] {
- let (output, value) = sandbox.json_output(args);
-
- assert!(
- output.status.success(),
- "`{args:?}` should remain observable under MYC mode: {value:?}"
- );
- assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
- }
-}
-
-#[test]
-fn local_listing_publish_fails_without_local_account_authority() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let listing_file = create_listing_draft(&sandbox, "local-no-account");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- account_id,
- ]);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000004",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_unresolved");
- assert_eq!(value["errors"][0]["exit_code"], 5);
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_contains(
- &value["errors"][0]["message"],
- "listing-bound seller account",
- );
-}
-
-#[test]
-fn local_listing_publish_dry_run_validates_local_account_authority() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let listing_file = create_listing_draft(&sandbox, "local-dry-run-no-account");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- account_id,
- ]);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_unresolved");
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_no_removed_command_reference(&value, &["listing", "publish", "--dry-run"]);
-}
-
-#[test]
-fn local_listing_update_dry_run_validates_local_account_authority() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let listing_file = create_listing_draft(&sandbox, "local-update-dry-run-no-account");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- account_id,
- ]);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "update",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.update");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_unresolved");
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_no_removed_command_reference(&value, &["listing", "update", "--dry-run"]);
-}
-
-#[test]
-fn local_listing_update_dry_run_rejects_mismatched_local_account() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "local-update-dry-run-mismatch");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--account-id",
- second_account_id,
- "--dry-run",
- "listing",
- "update",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert_ne!(
- first["result"]["account"]["id"],
- second["result"]["account"]["id"]
- );
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.update");
- assert_eq!(value["errors"][0]["code"], "account_mismatch");
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
-}
-
-#[test]
-fn local_listing_publish_fails_without_configured_relay() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "local-unavailable");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &value["errors"][0]["message"],
- "requires a delivery-capable transport profile",
- );
- assert_no_removed_command_reference(&value, &["listing", "publish"]);
- assert_no_daemon_runtime_reference(&value, &["listing", "publish"]);
-}
-
-#[test]
-fn local_listing_publish_dry_run_does_not_sign_matching_listing() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "local-dry-run");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["dry_run"], true);
- assert_eq!(
- value["result"]["event_id"]
- .as_str()
- .expect("dry-run event id")
- .len(),
- 64
- );
- assert!(
- !sandbox.root().join("data/apps/cli/replica/sdk").exists(),
- "dry-run must not materialize durable SDK storage"
- );
- assert_no_removed_command_reference(&value, &["listing", "publish", "--dry-run"]);
- assert_no_daemon_runtime_reference(&value, &["listing", "publish", "--dry-run"]);
-}
-
-#[test]
-fn local_listing_archive_dry_run_validates_local_account_authority() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "local-archive-mismatch");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--account-id",
- second_account_id,
- "--dry-run",
- "listing",
- "archive",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.archive");
- assert_eq!(value["errors"][0]["code"], "account_mismatch");
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
-}
-
-#[test]
-fn local_listing_publish_fails_when_selected_account_does_not_match_seller() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- let listing_file = create_listing_draft(&sandbox, "local-mismatch");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
- assert_ne!(first_account_id, second_account_id);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--account-id",
- second_account_id,
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_mismatch");
- assert_eq!(value["errors"][0]["exit_code"], 5);
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_contains(
- &value["errors"][0]["message"],
- "listing draft is bound to seller account",
- );
- assert_no_removed_command_reference(&value, &["listing", "publish", "account mismatch"]);
-}
-
-#[test]
-fn local_farm_publish_dry_run_validates_secret_backed_account() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let value = sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
-
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["dry_run"], true);
- assert_no_daemon_runtime_reference(&value, &["farm", "publish", "--dry-run"]);
-}
-
-#[test]
-fn local_farm_publish_dry_run_plans_without_configured_relay() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- let value = sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
-
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["dry_run"], true);
- assert_no_removed_command_reference(&value, &["farm", "publish", "--dry-run"]);
- assert_no_daemon_runtime_reference(&value, &["farm", "publish", "--dry-run"]);
-}
-
-#[test]
-fn local_farm_publish_fails_without_configured_relay() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000003",
- "farm",
- "publish",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &value["errors"][0]["message"],
- "requires a delivery-capable transport profile",
- );
- assert_no_removed_command_reference(&value, &["farm", "publish"]);
- assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
-}
-
-#[test]
-fn farm_setup_actions_offer_publish_before_and_after_delivery_configuration() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
-
- let unconfigured = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- assert_action_present(&unconfigured, "radroots farm readiness check");
- assert_action_present(&unconfigured, "radroots farm publish");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let configured = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "profile",
- "update",
- "--field",
- "name",
- "--value",
- "Green Farm Updated",
- ]);
-
- assert_action_present(&configured, "radroots farm readiness check");
- assert_action_present(&configured, "radroots farm publish");
-}
-
-#[test]
-fn farm_setup_actions_withhold_publish_for_watch_only_account() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(51);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "farm-watch-only", &public_identity);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let created = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Watch Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- assert_action_present(&created, "radroots farm readiness check");
- assert_action_absent(&created, "radroots farm publish");
-
- let updated = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "profile",
- "update",
- "--field",
- "name",
- "--value",
- "Watch Farm Updated",
- ]);
-
- assert_action_present(&updated, "radroots farm readiness check");
- assert_action_absent(&updated, "radroots farm publish");
-}
-
-#[test]
-fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let relay_url = "ws://127.0.0.1:9";
- let idempotency_key = "01890f3c-7f2a-7b01-8000-000000000001";
-
- sandbox.write_nostr_transport_profile(&[relay_url]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- idempotency_key,
- "farm",
- "publish",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &value["errors"][0]["message"],
- "SDK transport publish did not reach accepted quorum",
- );
- let detail = &value["errors"][0]["detail"];
- assert_eq!(detail["source"], "SDK farm publish · configured signer");
- assert_eq!(detail["state"], "unavailable");
- assert_eq!(detail["profile"]["state"], "not_submitted");
- assert_eq!(detail["farm"]["state"], "unavailable");
- assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
- assert_eq!(
- detail["farm"]["event_id"]
- .as_str()
- .expect("sdk farm event id")
- .len(),
- 64
- );
- assert_eq!(
- detail["profile"]["idempotency_key"],
- "01890f3c-7f2a-7b01-8000-9db91832364f"
- );
- assert_eq!(
- detail["farm"]["idempotency_key"],
- "01890f3c-7f2a-7b01-8000-f953bff1d106"
- );
- assert_eq!(detail["actions"][0], "radroots sync push");
- assert_eq!(detail["farm"]["target_transport_endpoints"][0], relay_url);
- assert_relay_url(
- &detail["farm"]["failed_transport_targets"][0]["endpoint_uri"],
- relay_url,
- );
- assert_no_removed_command_reference(&value, &["farm", "publish"]);
- assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
-
- let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]);
- assert_eq!(
- persisted["result"]["document"]["publication"]["profile_state"],
- "not_published"
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["farm_state"],
- "not_published"
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["profile_event_id"],
- serde_json::Value::Null
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["farm_event_id"],
- serde_json::Value::Null
- );
-}
-
-#[test]
-fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let relay_url = "ws://127.0.0.1:9";
- let idempotency_key = "01890f3c-7f2a-7b01-8000-000000000002";
-
- sandbox.write_nostr_transport_profile(&[relay_url]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- idempotency_key,
- "farm",
- "publish",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "network");
- let detail = &value["errors"][0]["detail"];
- assert_eq!(detail["source"], "SDK farm publish · configured signer");
- assert_eq!(detail["profile"]["state"], "not_submitted");
- assert_eq!(detail["farm"]["state"], "unavailable");
- assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
- assert_eq!(
- detail["farm"]["event_id"]
- .as_str()
- .expect("sdk farm event id")
- .len(),
- 64
- );
- assert_eq!(
- detail["profile"]["idempotency_key"],
- "01890f3c-7f2a-7b01-8000-c7f426f40246"
- );
- assert_eq!(
- detail["farm"]["idempotency_key"],
- "01890f3c-7f2a-7b01-8000-692916b32301"
- );
- assert_no_removed_command_reference(&value, &["farm", "publish"]);
- assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
-
- let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]);
- assert_eq!(
- persisted["result"]["document"]["publication"]["profile_state"],
- "not_published"
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["farm_state"],
- "not_published"
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["profile_event_id"],
- serde_json::Value::Null
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["farm_event_id"],
- serde_json::Value::Null
- );
-}
-
-#[test]
-fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_path = farm["result"]["config"]["path"]
- .as_str()
- .expect("farm path");
- let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag");
- let first_pubkey = farm["result"]["config"]["seller_pubkey"]
- .as_str()
- .expect("first pubkey");
- assert_eq!(
- farm["result"]["config"]["seller_account_id"],
- first_account_id
- );
- assert_eq!(farm["result"]["config"]["seller_pubkey"], first_pubkey);
- assert_eq!(
- farm["result"]["config"]["seller_actor_source"],
- "farm_config"
- );
- assert!(
- farm["result"]["config"]
- .get("selected_account_id")
- .is_none()
- );
-
- let published = sandbox.json_success(&["--format", "json", "farm", "get"]);
- assert_eq!(
- published["result"]["document"]["publication"]["profile_state"],
- "not_published"
- );
- assert_eq!(
- published["result"]["document"]["publication"]["farm_state"],
- "not_published"
- );
-
- let same_seller_dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "rebind",
- first_account_id,
- ]);
- assert_eq!(same_seller_dry_run["operation_id"], "farm.rebind");
- assert_eq!(
- same_seller_dry_run["result"]["publication_state_action"],
- "preserved"
- );
-
- let same_seller_live = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "farm",
- "rebind",
- first_account_id,
- ]);
- assert_eq!(same_seller_live["operation_id"], "farm.rebind");
- assert_eq!(
- same_seller_live["result"]["publication_state_action"],
- "preserved"
- );
- let same_seller_get = sandbox.json_success(&["--format", "json", "farm", "get"]);
- assert_eq!(
- same_seller_get["result"]["document"]["publication"]["profile_state"],
- "not_published"
- );
- assert_eq!(
- same_seller_get["result"]["document"]["publication"]["farm_state"],
- "not_published"
- );
-
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
- assert_ne!(first_account_id, second_account_id);
- sandbox.json_success(&[
- "--format",
- "json",
- "account",
- "selection",
- "update",
- second_account_id,
- ]);
-
- let (retarget_output, retarget) = sandbox.json_output(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm Retarget",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- assert!(!retarget_output.status.success());
- assert_eq!(retarget["operation_id"], "farm.create");
- assert_eq!(retarget["errors"][0]["code"], "account_mismatch");
- assert_contains(&retarget["errors"][0]["message"], "farm-bound seller");
- assert_eq!(
- retarget["errors"][0]["detail"]["seller_actor_source"],
- "farm_config"
- );
- assert_eq!(
- retarget["errors"][0]["detail"]["farm_bound_seller_account_id"],
- first_account_id
- );
- assert_eq!(
- retarget["errors"][0]["detail"]["attempted_seller_account_id"],
- second_account_id
- );
- assert_next_action_present(
- &retarget,
- format!("radroots --dry-run farm rebind {second_account_id}").as_str(),
- );
- assert_next_action_present(
- &retarget,
- format!("radroots --approval-token approve farm rebind {second_account_id}").as_str(),
- );
-
- let terminal_retarget_output = sandbox
- .command()
- .args([
- "farm",
- "create",
- "--name",
- "Green Farm Retarget",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ])
- .output()
- .expect("terminal retarget");
- assert!(!terminal_retarget_output.status.success());
- assert!(terminal_retarget_output.stdout.is_empty());
- let terminal_stderr =
- String::from_utf8(terminal_retarget_output.stderr).expect("terminal stderr");
- assert!(
- terminal_stderr.contains("Next\n radroots --dry-run farm rebind"),
- "{terminal_stderr}"
- );
- assert!(
- terminal_stderr.contains(
- format!("radroots --approval-token approve farm rebind {second_account_id}").as_str(),
- ),
- "{terminal_stderr}"
- );
-
- let (missing_rebind_output, missing_rebind) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "rebind",
- "acct_missing",
- ]);
- assert!(!missing_rebind_output.status.success());
- assert_eq!(missing_rebind["operation_id"], "farm.rebind");
- assert_eq!(missing_rebind["errors"][0]["code"], "account_unresolved");
- assert_eq!(
- missing_rebind["errors"][0]["detail"]["seller_actor_source"],
- "farm_config"
- );
- assert_eq!(
- missing_rebind["errors"][0]["detail"]["selector"],
- "acct_missing"
- );
- assert_next_action_present(&missing_rebind, "radroots account import <path>");
- assert_next_action_present(&missing_rebind, "radroots account create");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let publish_dry_run =
- sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
- assert_eq!(publish_dry_run["operation_id"], "farm.publish");
- assert_eq!(publish_dry_run["result"]["state"], "dry_run");
- assert_eq!(
- publish_dry_run["result"]["seller_account_id"],
- first_account_id
- );
- assert_eq!(publish_dry_run["result"]["seller_pubkey"], first_pubkey);
- assert!(
- publish_dry_run["result"]
- .get("selected_account_id")
- .is_none()
- );
-
- let listing_path = sandbox.root().join("drift-listing.toml");
- let listing = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "create",
- "--output",
- listing_path.to_string_lossy().as_ref(),
- "--key",
- "drift-eggs",
- "--title",
- "Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- assert_eq!(listing["operation_id"], "listing.create");
- assert_eq!(listing["result"]["seller_pubkey"], first_pubkey);
- assert_eq!(listing["result"]["farm_d_tag"], farm_d_tag);
-
- let farm_before_dry_run = fs::read_to_string(farm_path).expect("farm before dry-run rebind");
- let dry_rebind = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "rebind",
- second_account_id,
- ]);
- assert_eq!(dry_rebind["operation_id"], "farm.rebind");
- assert_eq!(dry_rebind["result"]["state"], "dry_run");
- assert_eq!(
- dry_rebind["result"]["from_seller_account_id"],
- first_account_id
- );
- assert_eq!(dry_rebind["result"]["from_seller_pubkey"], first_pubkey);
- assert_eq!(
- dry_rebind["result"]["to_seller_account_id"],
- second_account_id
- );
- let second_pubkey = dry_rebind["result"]["to_seller_pubkey"]
- .as_str()
- .expect("second pubkey");
- assert_eq!(dry_rebind["result"]["to_seller_pubkey"], second_pubkey);
- assert_eq!(dry_rebind["result"]["seller_pubkey_changed"], true);
- assert_eq!(dry_rebind["result"]["publication_state_action"], "cleared");
- assert_eq!(
- fs::read_to_string(farm_path).expect("farm after dry-run rebind"),
- farm_before_dry_run
- );
-
- let (unapproved_output, unapproved) =
- sandbox.json_output(&["--format", "json", "farm", "rebind", second_account_id]);
- assert!(!unapproved_output.status.success());
- assert_eq!(unapproved["operation_id"], "farm.rebind");
- assert_eq!(unapproved["errors"][0]["code"], "approval_required");
-
- let rebound = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "farm",
- "rebind",
- second_account_id,
- ]);
- assert_eq!(rebound["operation_id"], "farm.rebind");
- assert_eq!(rebound["result"]["state"], "rebound");
- assert_eq!(
- rebound["result"]["config"]["seller_account_id"],
- second_account_id
- );
- assert_eq!(rebound["result"]["config"]["seller_pubkey"], second_pubkey);
- assert_eq!(rebound["result"]["config"]["farm_d_tag"], farm_d_tag);
- assert_eq!(rebound["result"]["config"]["name"], "Green Farm");
- assert_eq!(rebound["result"]["config"]["location_primary"], "farmstand");
- assert_eq!(rebound["result"]["config"]["delivery_method"], "pickup");
- assert_eq!(rebound["result"]["publication_state_action"], "cleared");
-
- let rebound_get = sandbox.json_success(&["--format", "json", "farm", "get"]);
- assert_eq!(
- rebound_get["result"]["document"]["selection"]["seller_account_id"],
- second_account_id
- );
- assert_eq!(
- rebound_get["result"]["document"]["publication"]["profile_state"],
- "not_published"
- );
- assert_eq!(
- rebound_get["result"]["document"]["publication"]["farm_state"],
- "not_published"
- );
-}
-
-#[test]
-fn missing_farm_bound_seller_blocks_listing_create_and_guides_setup_repair() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Missing Seller Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
- sandbox.json_success(&[
- "--format",
- "json",
- "account",
- "selection",
- "update",
- second_account_id,
- ]);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- first_account_id,
- ]);
-
- let updated = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "profile",
- "update",
- "--field",
- "name",
- "--value",
- "Missing Seller Farm Updated",
- ]);
- assert_eq!(updated["operation_id"], "farm.profile.update");
- assert_contains(&updated["result"]["reason"], "farm-bound seller account");
- assert_action_present(&updated, "radroots account import <path>");
- assert_action_present(&updated, "radroots --dry-run farm rebind <selector>");
- assert_action_present(
- &updated,
- "radroots --approval-token approve farm rebind <selector>",
- );
-
- let listing_path = sandbox.root().join("missing-seller-listing.toml");
- let (listing_output, listing) = sandbox.json_output(&[
- "--format",
- "json",
- "listing",
- "create",
- "--output",
- listing_path.to_string_lossy().as_ref(),
- "--key",
- "missing-seller-eggs",
- "--title",
- "Missing Seller Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- assert!(!listing_output.status.success());
- assert_eq!(listing["operation_id"], "listing.create");
- assert_eq!(listing["errors"][0]["code"], "account_unresolved");
- assert_contains(
- &listing["errors"][0]["message"],
- "farm-bound seller account",
- );
- assert_eq!(
- listing["errors"][0]["detail"]["seller_actor_source"],
- "farm_config"
- );
- assert_eq!(
- listing["errors"][0]["detail"]["farm_bound_seller_account_id"],
- first_account_id
- );
- assert_next_action_present(&listing, "radroots account import <path>");
- assert_next_action_present(&listing, "radroots --dry-run farm rebind <selector>");
- assert_next_action_present(
- &listing,
- "radroots --approval-token approve farm rebind <selector>",
- );
- assert!(!listing_path.exists());
-}
-
-#[test]
-fn farm_rebind_allows_watch_only_target_and_attach_secret_recovers_publish() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Watch Rebind Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let watch_identity = identity_secret(56);
- let watch_public = watch_identity.to_public();
- let public_identity_file =
- write_public_identity_profile(&sandbox, "watch-rebind-public", &watch_public);
- let secret_identity_file =
- write_secret_identity_profile(&sandbox, "watch-rebind-secret", &watch_identity);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let watch_account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("watch account id");
- assert_eq!(imported["result"]["account"]["custody"], "watch_only");
-
- let rebound = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "farm",
- "rebind",
- watch_account_id,
- ]);
- assert_eq!(rebound["operation_id"], "farm.rebind");
- assert_eq!(
- rebound["result"]["config"]["seller_account_id"],
- watch_account_id
- );
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let readiness = sandbox.json_success(&["--format", "json", "farm", "readiness", "check"]);
- assert_eq!(readiness["operation_id"], "farm.readiness.check");
- assert_eq!(readiness["result"]["publish_state"], "unconfigured");
- assert_eq!(
- readiness["result"]["missing"][0],
- "Write-capable farm-bound seller account"
- );
- assert_action_present(
- &readiness,
- format!("radroots account attach-secret {watch_account_id} <path>").as_str(),
- );
-
- let (publish_output, publish) =
- sandbox.json_output(&["--format", "json", "--dry-run", "farm", "publish"]);
- assert!(!publish_output.status.success());
- assert_eq!(publish["operation_id"], "farm.publish");
- assert_eq!(publish["errors"][0]["code"], "account_watch_only");
-
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "attach-secret",
- watch_account_id,
- secret_identity_file.to_string_lossy().as_ref(),
- ]);
- let recovered = sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
- assert_eq!(recovered["operation_id"], "farm.publish");
- assert_eq!(recovered["result"]["state"], "dry_run");
- assert_eq!(recovered["result"]["seller_account_id"], watch_account_id);
- assert_eq!(
- recovered["result"]["seller_pubkey"],
- watch_public.public_key_hex
- );
-}
-
-#[test]
-fn local_seller_publish_commands_attempt_configured_relay() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag");
- let relay = "ws://127.0.0.1:9";
-
- sandbox.write_nostr_transport_profile(&[relay]);
- let (farm_output, farm_value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000003",
- "farm",
- "publish",
- ]);
- assert!(!farm_output.status.success());
- assert_eq!(farm_value["operation_id"], "farm.publish");
- assert_eq!(farm_value["result"], serde_json::Value::Null);
- assert_eq!(farm_value["errors"][0]["code"], "network_unavailable");
- assert_eq!(farm_value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &farm_value["errors"][0]["message"],
- "SDK transport publish did not reach accepted quorum",
- );
- assert_eq!(
- farm_value["errors"][0]["detail"]["source"],
- "SDK farm publish · configured signer"
- );
- assert_eq!(
- farm_value["errors"][0]["detail"]["farm"]["target_transport_endpoints"][0],
- relay
- );
- assert_eq!(
- farm_value["errors"][0]["detail"]["farm"]["failed_transport_targets"][0]["endpoint_uri"],
- relay
- );
- assert_no_removed_command_reference(&farm_value, &["farm", "publish"]);
- assert_no_daemon_runtime_reference(&farm_value, &["farm", "publish"]);
-
- let listing_file = create_listing_draft(&sandbox, "direct-relay-attempt");
- make_listing_publishable(&listing_file, farm_d_tag);
- let listing_file_arg = listing_file.to_string_lossy();
-
- let (publish_output, publish_value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000004",
- "listing",
- "publish",
- listing_file_arg.as_ref(),
- ]);
- assert!(!publish_output.status.success());
- assert_eq!(publish_value["operation_id"], "listing.publish");
- assert_eq!(publish_value["result"], serde_json::Value::Null);
- assert_eq!(publish_value["errors"][0]["code"], "network_unavailable");
- assert_eq!(publish_value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &publish_value["errors"][0]["message"],
- "SDK transport publish did not reach accepted quorum",
- );
- assert_no_removed_command_reference(&publish_value, &["listing", "publish"]);
- assert_no_daemon_runtime_reference(&publish_value, &["listing", "publish"]);
- assert_eq!(
- publish_value["errors"][0]["detail"]["target_transport_endpoints"][0],
- relay
- );
- assert_eq!(
- publish_value["errors"][0]["detail"]["attempted_transport_endpoints"]
- .as_array()
- .expect("connected relays")
- .len(),
- 1
- );
- assert_eq!(
- publish_value["errors"][0]["detail"]["failed_transport_targets"]
- .as_array()
- .expect("failed relays")
- .len(),
- 1
- );
-
- let (archive_output, archive_value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000005",
- "listing",
- "archive",
- listing_file_arg.as_ref(),
- ]);
- assert!(!archive_output.status.success());
- assert_eq!(archive_value["operation_id"], "listing.archive");
- assert_eq!(archive_value["result"], serde_json::Value::Null);
- assert_eq!(archive_value["errors"][0]["code"], "network_unavailable");
- assert_eq!(archive_value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &archive_value["errors"][0]["message"],
- "SDK transport publish did not reach accepted quorum",
- );
- assert_no_removed_command_reference(&archive_value, &["listing", "archive"]);
- assert_no_daemon_runtime_reference(&archive_value, &["listing", "archive"]);
- assert_eq!(
- archive_value["errors"][0]["detail"]["target_transport_endpoints"][0],
- relay
- );
- assert_eq!(
- archive_value["errors"][0]["detail"]["attempted_transport_endpoints"]
- .as_array()
- .expect("connected relays")
- .len(),
- 1
- );
- assert_eq!(
- archive_value["errors"][0]["detail"]["failed_transport_targets"]
- .as_array()
- .expect("failed relays")
- .len(),
- 1
- );
-
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- sandbox.json_success(&["--format", "json", "basket", "create", "direct_order"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "direct_order",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "1",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "direct_order",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- let (order_output, order_value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "trade",
- "submit",
- order_id,
- ]);
- assert!(!order_output.status.success());
- assert_eq!(order_value["operation_id"], "trade.submit");
- assert_eq!(order_value["result"], serde_json::Value::Null);
- assert_eq!(order_value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(order_value["errors"][0]["detail"]["class"], "operation");
- assert_eq!(
- order_value["errors"][0]["detail"]["operation_id"],
- "trade.submit"
- );
- assert_contains(
- &order_value["errors"][0]["message"],
- "trade draft is not ready for submit",
- );
- assert_eq!(
- order_value["errors"][0]["detail"]["issues"][0]["code"],
- "listing_provenance_missing"
- );
- assert_no_removed_command_reference(&order_value, &["trade", "submit"]);
- assert_no_daemon_runtime_reference(&order_value, &["trade", "submit"]);
-}
-
-#[test]
-fn local_order_event_list_attempts_configured_shared_nostr_transport() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let relay = "ws://127.0.0.1:9";
- sandbox.write_nostr_transport_profile(&[relay]);
-
- let (output, value) = sandbox.json_output(&["--format", "json", "trade", "event", "list"]);
-
- assert!(!output.status.success());
- assert_transport_fetch_failure(&value, "trade.event.list", &["trade", "event", "list"]);
- assert_eq!(value["errors"][0]["detail"]["state"], "unavailable");
- assert_eq!(
- value["errors"][0]["detail"]["target_transport_endpoints"][0],
- relay
- );
- assert_eq!(
- value["errors"][0]["detail"]["attempted_transport_endpoints"]
- .as_array()
- .expect("connected relays")
- .len(),
- 0
- );
- assert_eq!(
- value["errors"][0]["detail"]["failed_transport_targets"]
- .as_array()
- .expect("failed relays")
- .len(),
- 1
- );
- assert_contains(
- &value["errors"][0]["detail"]["failed_transport_targets"][0]["endpoint_uri"],
- "127.0.0.1:9",
- );
- assert_eq!(value["errors"][0]["detail"]["fetched_count"], 0);
- assert_eq!(value["errors"][0]["detail"]["decoded_count"], 0);
- assert_eq!(value["errors"][0]["detail"]["skipped_count"], 0);
-}
-
-#[test]
-fn local_order_failure_envelopes_are_structured_and_actionable() {
- let sandbox = RadrootsCliSandbox::new();
- let watch_args = ["--format", "json", "trade", "event", "watch", "ord_missing"];
- let (watch_output, watch) = sandbox.json_output(&watch_args);
- assert!(!watch_output.status.success());
- assert_eq!(watch["operation_id"], "trade.event.watch");
- assert_eq!(watch["result"], Value::Null);
- assert_eq!(watch["errors"][0]["code"], "not_implemented");
- assert_eq!(watch["errors"][0]["detail"]["state"], "not_implemented");
- assert_eq!(watch["errors"][0]["detail"]["trade_id"], "ord_missing");
- assert_eq!(
- watch["next_actions"][0]["command"],
- "radroots trade status get ord_missing"
- );
- assert_no_daemon_runtime_reference(&watch, &watch_args);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let submit_args = [
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- "ord_missing",
- ];
- let (submit_output, submit) = sandbox.json_output(&submit_args);
- assert!(!submit_output.status.success());
- assert_eq!(submit["errors"][0]["code"], "not_found");
- assert_eq!(submit["errors"][0]["detail"]["state"], "missing");
- assert_eq!(submit["errors"][0]["detail"]["trade_id"], "ord_missing");
- assert_eq!(submit["next_actions"][0]["command"], "radroots trade list");
- assert_eq!(
- submit["next_actions"][1]["command"],
- "radroots basket create"
- );
- assert_no_daemon_runtime_reference(&submit, &submit_args);
-
- let status_args = ["--format", "json", "trade", "status", "get", "ord_missing"];
- let status = sandbox.json_success(&status_args);
- assert_eq!(status["operation_id"], "trade.status.get");
- assert_eq!(status["result"]["state"], "missing");
- assert_eq!(status["result"]["source"], "SDK local trade projection");
- assert_eq!(
- status["result"]["actor_context_source"],
- "sdk_local_projection"
- );
- assert_eq!(status["result"]["trade_id"], "ord_missing");
- assert_eq!(status["result"]["fetched_count"], 0);
- assert_eq!(status["result"]["decoded_count"], 0);
- assert_eq!(
- status["result"]["reason"],
- "no local SDK trade events matched `ord_missing`"
- );
- assert_no_daemon_runtime_reference(&status, &status_args);
-
- sandbox.write_app_config("[transport]\nprofile = \"local_only\"\n");
- let event_list_no_relay_args = ["--format", "json", "trade", "event", "list"];
- let (event_list_no_relay_output, event_list_no_relay) =
- sandbox.json_output(&event_list_no_relay_args);
- assert!(!event_list_no_relay_output.status.success());
- assert_eq!(
- event_list_no_relay["errors"][0]["code"],
- "operation_unavailable"
- );
- assert_eq!(
- event_list_no_relay["errors"][0]["detail"]["state"],
- "unconfigured"
- );
- assert_eq!(
- event_list_no_relay["next_actions"][0]["command"],
- "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
- );
- assert_no_daemon_runtime_reference(&event_list_no_relay, &event_list_no_relay_args);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let event_list_no_account_args = ["--format", "json", "trade", "event", "list"];
- let (event_list_no_account_output, event_list_no_account) =
- sandbox.json_output(&event_list_no_account_args);
- assert!(!event_list_no_account_output.status.success());
- assert_eq!(
- event_list_no_account["errors"][0]["code"],
- "operation_unavailable"
- );
- assert_eq!(
- event_list_no_account["errors"][0]["detail"]["state"],
- "unconfigured"
- );
- assert_eq!(
- event_list_no_account["next_actions"][0]["command"],
- "radroots account create"
- );
- assert_no_daemon_runtime_reference(&event_list_no_account, &event_list_no_account_args);
-
- let accept_args = [
- "--format",
- "json",
- "--dry-run",
- "trade",
- "accept",
- "ord_missing",
- ];
- let (accept_output, accept) = sandbox.json_output(&accept_args);
- assert!(!accept_output.status.success());
- assert_eq!(accept["errors"][0]["code"], "operation_unavailable");
- assert_eq!(accept["errors"][0]["detail"]["state"], "unconfigured");
- assert_eq!(accept["errors"][0]["detail"]["trade_id"], "ord_missing");
- assert_eq!(accept["errors"][0]["detail"]["decision"], "accepted");
- assert_no_daemon_runtime_reference(&accept, &accept_args);
-
- let decline_args = [
- "--format",
- "json",
- "--dry-run",
- "trade",
- "decline",
- "ord_missing",
- "--reason",
- "not available",
- ];
- let (decline_output, decline) = sandbox.json_output(&decline_args);
- assert!(!decline_output.status.success());
- assert_eq!(decline["errors"][0]["code"], "operation_unavailable");
- assert_eq!(decline["errors"][0]["detail"]["state"], "unconfigured");
- assert_eq!(decline["errors"][0]["detail"]["trade_id"], "ord_missing");
- assert_eq!(decline["errors"][0]["detail"]["decision"], "declined");
- assert_no_daemon_runtime_reference(&decline, &decline_args);
-}
-
-#[test]
-fn watch_only_farm_publish_dry_run_fails_as_account_watch_only() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(13);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "watch-only-farm", &public_identity);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) =
- sandbox.json_output(&["--format", "json", "--dry-run", "farm", "publish"]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["errors"][0]["code"], "account_watch_only");
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
-}
-
-#[test]
-fn watch_only_listing_publish_fails_as_account_watch_only() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(12);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "watch-only-publish", &public_identity);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let listing_file = create_listing_draft(&sandbox, "watch-only-publish");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_watch_only");
- assert_eq!(value["errors"][0]["exit_code"], 7);
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_contains(&value["errors"][0]["message"], "resolved account");
- assert_contains(&value["errors"][0]["message"], "watch_only");
-}
-
-#[test]
-fn watch_only_listing_update_dry_run_fails_as_account_watch_only() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(13);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "watch-only-update", &public_identity);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let listing_file = create_listing_draft(&sandbox, "watch-only-update");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
-
- sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "update",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.update");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_watch_only");
- assert_eq!(value["errors"][0]["exit_code"], 7);
- assert_eq!(value["errors"][0]["detail"]["class"], "account");
- assert_contains(&value["errors"][0]["message"], "watch_only");
-}
-
-#[cfg(unix)]
-#[test]
-fn myc_listing_publish_does_not_fallback_to_local_account() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "myc-no-binding");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- let invoked = sandbox.root().join("myc-listing-invoked.txt");
- let myc = sandbox.write_fake_myc(
- "myc-listing-deferred",
- format!(
- "printf invoked > '{}'",
- shell_single_quoted(invoked.to_string_lossy().as_ref())
- )
- .as_str(),
- );
- sandbox.write_app_config(&format!(
- "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"ws://127.0.0.1:9\"]\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
- toml_string(myc.display().to_string().as_str())
- ));
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "signer_unconfigured");
- assert_eq!(value["errors"][0]["exit_code"], 7);
- assert_eq!(value["errors"][0]["detail"]["class"], "signer");
- assert_contains(
- &value["errors"][0]["message"],
- "signer.remote_nip46 binding is missing",
- );
- assert!(!invoked.exists(), "target CLI must not execute MYC");
-}
-
-fn configure_myc_mode(sandbox: &RadrootsCliSandbox, executable: &Path) {
- sandbox.write_app_config(&format!(
- "[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
- toml_string(executable.display().to_string().as_str())
- ));
-}
-
-fn assert_transport_fetch_failure(value: &serde_json::Value, operation_id: &str, args: &[&str]) {
- assert_eq!(value["operation_id"], operation_id);
- assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_ne!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &value["errors"][0]["message"],
- "Nostr transport fetch failed",
- );
- assert_no_removed_command_reference(value, args);
- assert_no_daemon_runtime_reference(value, args);
-}
-
-fn assert_relay_url(value: &Value, relay_url: &str) {
- let actual = value.as_str().expect("relay url");
- assert!(
- actual == relay_url || actual == format!("{relay_url}/"),
- "expected relay url `{actual}` to match `{relay_url}`"
- );
-}
-
-fn assert_action_present(value: &Value, action: &str) {
- assert!(
- action_list(value).iter().any(|entry| *entry == action),
- "expected action `{action}` in `{}`",
- value["result"]["actions"]
- );
-}
-
-fn assert_next_action_present(value: &Value, action: &str) {
- assert!(
- next_action_commands(value)
- .iter()
- .any(|entry| *entry == action),
- "expected next action `{action}` in `{}`",
- value["next_actions"]
- );
-}
-
-fn assert_action_absent(value: &Value, action: &str) {
- assert!(
- action_list(value).iter().all(|entry| *entry != action),
- "did not expect action `{action}` in `{}`",
- value["result"]["actions"]
- );
-}
-
-fn action_list(value: &Value) -> Vec<&str> {
- value["result"]["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .map(|entry| entry.as_str().expect("action"))
- .collect()
-}
-
-fn next_action_commands(value: &Value) -> Vec<&str> {
- value["next_actions"]
- .as_array()
- .expect("next actions")
- .iter()
- .filter_map(|entry| entry["command"].as_str())
- .collect()
-}
-
-fn create_test_farm(sandbox: &RadrootsCliSandbox) -> Value {
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ])
-}
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -1,747 +0,0 @@
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::{Command, Output};
-use std::sync::Mutex;
-
-use assert_cmd::prelude::*;
-use radroots_event::ids::RadrootsListingAddress;
-use radroots_event::kinds::{KIND_FARM, KIND_LISTING};
-use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts};
-use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
-use radroots_protected_store::RadrootsProtectedFileSecretVault;
-use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
-use radroots_runtime_store::{
- PublishOutboxStatus, RuntimeStore, RuntimeStoreRecord, RuntimeStoreRecordFamily,
- RuntimeStoreRecordInput, RuntimeStoreRecordStatus, SourceRuntime,
-};
-use radroots_secret_vault::RadrootsSecretVault;
-use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor};
-use serde_json::{Value, json};
-use tempfile::TempDir;
-
-#[cfg(unix)]
-use std::os::unix::fs::PermissionsExt;
-
-static COMMAND_LOCK: Mutex<()> = Mutex::new(());
-
-pub fn radroots() -> Command {
- Command::cargo_bin("radroots").expect("binary")
-}
-
-pub fn json_from_stdout(output: &Output) -> Value {
- serde_json::from_slice(&output.stdout).unwrap_or_else(|error| {
- panic!(
- "stdout was not json: {error}; stderr `{}`; stdout `{}`",
- String::from_utf8_lossy(&output.stderr),
- String::from_utf8_lossy(&output.stdout)
- )
- })
-}
-
-pub fn ndjson_from_stdout(output: &Output) -> Vec<Value> {
- let stdout = String::from_utf8_lossy(&output.stdout);
- let frames = stdout
- .lines()
- .filter(|line| !line.trim().is_empty())
- .map(|line| {
- serde_json::from_str::<Value>(line).unwrap_or_else(|error| {
- panic!(
- "stdout line was not json: {error}; stderr `{}`; line `{line}`; stdout `{stdout}`",
- String::from_utf8_lossy(&output.stderr)
- )
- })
- })
- .collect::<Vec<_>>();
- assert!(!frames.is_empty(), "stdout should contain ndjson frames");
- frames
-}
-
-pub struct RadrootsCliSandbox {
- root: TempDir,
-}
-
-impl RadrootsCliSandbox {
- pub fn new() -> Self {
- Self {
- root: TempDir::new().expect("tempdir"),
- }
- }
-
- pub fn root(&self) -> &Path {
- self.root.path()
- }
-
- pub fn command(&self) -> Command {
- let mut command = radroots();
- self.apply_base_env(&mut command);
- command
- }
-
- pub fn json_success(&self, args: &[&str]) -> Value {
- let _guard = COMMAND_LOCK.lock().expect("cli command lock");
- let output = self.command().args(args).output().expect("run command");
- assert!(
- output.status.success(),
- "`{args:?}` failed with stderr `{}` and stdout `{}`",
- String::from_utf8_lossy(&output.stderr),
- String::from_utf8_lossy(&output.stdout)
- );
- json_from_stdout(&output)
- }
-
- pub fn json_output(&self, args: &[&str]) -> (Output, Value) {
- let _guard = COMMAND_LOCK.lock().expect("cli command lock");
- let output = self.command().args(args).output().expect("run command");
- let value = json_from_stdout(&output);
- (output, value)
- }
-
- pub fn write_workspace_config(&self, raw: &str) -> PathBuf {
- let path = self.root.path().join("config.toml");
- fs::write(&path, raw).expect("write workspace config");
- path
- }
-
- pub fn write_app_config(&self, raw: &str) -> PathBuf {
- let path = self.root.path().join("config/apps/cli/config.toml");
- fs::create_dir_all(path.parent().expect("app config parent")).expect("app config dir");
- fs::write(&path, raw).expect("write app config");
- path
- }
-
- pub fn write_nostr_transport_profile(&self, relay_urls: &[&str]) -> PathBuf {
- let relays = relay_urls
- .iter()
- .map(|relay| format!("\"{}\"", relay.replace('"', "\\\"")))
- .collect::<Vec<_>>()
- .join(", ");
- self.write_app_config(
- format!(
- "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [{relays}]\n"
- )
- .as_str(),
- )
- }
-
- pub fn replica_store_path(&self) -> PathBuf {
- self.root
- .path()
- .join("data/apps/cli/replica/replica.sqlite")
- }
-
- pub fn runtime_store_db_path(&self) -> PathBuf {
- self.root
- .path()
- .join("data/shared/runtime_store/runtime_store.sqlite")
- }
-
- pub fn runtime_store_records(&self) -> Vec<RuntimeStoreRecord> {
- let path = self.runtime_store_db_path();
- if !path.exists() {
- return Vec::new();
- }
- let executor = SqlxSqliteExecutor::open(path).expect("open runtime store db");
- let store = RuntimeStore::new(executor);
- store.migrate_up().expect("migrate runtime store db");
- store
- .list_records_after_seq(0, 200)
- .expect("list local event records")
- }
-
- #[cfg(unix)]
- pub fn write_fake_myc(&self, name: &str, body: &str) -> PathBuf {
- let path = self.root.path().join("bin").join(name);
- fs::create_dir_all(path.parent().expect("fake myc parent")).expect("fake myc dir");
- fs::write(&path, format!("#!/bin/sh\nset -eu\n{body}\n")).expect("write fake myc");
- let mut permissions = fs::metadata(&path)
- .expect("fake myc metadata")
- .permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("fake myc executable");
- path
- }
-
- fn apply_base_env(&self, command: &mut Command) {
- command.env("RADROOTS_CLI_PATHS_PROFILE", "repo_local");
- command.env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", self.root.path());
- command.env("RADROOTS_CLI_ACCOUNT_SECRET_BACKEND", "encrypted_file");
- }
-}
-
-const _: () = {
- let _ = ndjson_from_stdout as fn(&Output) -> Vec<Value>;
- let _ = RadrootsCliSandbox::write_workspace_config as fn(&RadrootsCliSandbox, &str) -> PathBuf;
- let _ = RadrootsCliSandbox::write_nostr_transport_profile
- as fn(&RadrootsCliSandbox, &[&str]) -> PathBuf;
- let _ = RadrootsCliSandbox::replica_store_path as fn(&RadrootsCliSandbox) -> PathBuf;
- let _ = RadrootsCliSandbox::runtime_store_records
- as fn(&RadrootsCliSandbox) -> Vec<RuntimeStoreRecord>;
- #[cfg(unix)]
- let _ = RadrootsCliSandbox::write_fake_myc as fn(&RadrootsCliSandbox, &str, &str) -> PathBuf;
- let _ = assert_hex_len as fn(&Value, usize);
- let _ = remove_orderable_listing as fn(&RadrootsCliSandbox, &str);
- let _ = update_orderable_listing_available_amount as fn(&RadrootsCliSandbox, &str, i64);
- let _ = update_orderable_listing_primary_bin_id as fn(&RadrootsCliSandbox, &str, Option<&str>);
- let _ = duplicate_orderable_listing_row as fn(&RadrootsCliSandbox, &str);
- let _ = replace_latest_listing_event_id as fn(&RadrootsCliSandbox, &str, &str);
- let _ = store_test_session_secret as fn(&RadrootsCliSandbox, &str, &str);
- let _ = make_listing_publishable_with_seller as fn(&Path, &str, &str);
- let _ = shell_single_quoted as fn(&str) -> String;
-};
-
-pub fn assert_no_removed_command_reference(value: &Value, args: &[&str]) {
- let raw = serde_json::to_string(value).expect("json value");
- for removed in [
- "radroots setup",
- "radroots status",
- "radroots doctor",
- "radroots sell",
- "radroots find",
- "radroots local",
- "radroots net",
- "radroots myc",
- "radroots rpc",
- "radroots account new",
- "radroots config show",
- "radroots runtime status get",
- "radroots runtime start",
- "radroots runtime stop",
- "radroots runtime restart",
- "radroots runtime log watch",
- "radroots runtime config get",
- "radroots runtime config show",
- "radroots runtime install",
- "radroots runtime uninstall",
- "radroots runtime config set",
- "radroots signer session",
- "myc status",
- "radroots job get",
- "radroots job list",
- "radroots job watch",
- "radroots job cancel",
- "radroots job retry",
- "radroots market search",
- "radroots market view",
- "radroots market update",
- "radroots trade ls",
- "radroots trade history",
- "radroots trade watch",
- "radroots trade new",
- "radroots trade create",
- "radroots farm init",
- "radroots farm check",
- "radroots relay ls",
- "radroots product",
- "radroots message",
- "radroots approval",
- "radroots agent",
- ] {
- assert!(
- !raw.contains(removed),
- "`{args:?}` output should not contain removed command reference `{removed}`: {raw}"
- );
- }
-}
-
-pub fn assert_no_daemon_runtime_reference(value: &Value, args: &[&str]) {
- let raw = serde_json::to_string(value).expect("json value");
- for removed in ["radrootsd", "daemon", "bridge", "radroots job"] {
- assert!(
- !raw.contains(removed),
- "`{args:?}` output should not contain daemon runtime reference `{removed}`: {raw}"
- );
- }
-}
-
-pub fn assert_contains(value: &Value, needle: &str) {
- let value = value.as_str().expect("string value");
- assert!(
- value.contains(needle),
- "expected `{value}` to contain `{needle}`"
- );
-}
-
-pub fn assert_hex_len(value: &Value, expected_len: usize) {
- let value = value.as_str().expect("hex string");
- assert_eq!(value.len(), expected_len);
- assert!(value.chars().all(|ch| ch.is_ascii_hexdigit()));
-}
-
-pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) -> String {
- let store = sandbox.json_success(&["--format", "json", "store", "init"]);
- let db_path = store["result"]["path"]
- .as_str()
- .expect("replica db path from store init");
- let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr);
- let event_id = "2".repeat(64);
- let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: event_id.clone(),
- author: seller_pubkey.clone(),
- created_at: 1,
- kind: KIND_LISTING,
- tags: vec![
- vec!["d".to_owned(), listing_id],
- vec![
- "a".to_owned(),
- format!(
- "{}:{}:{}",
- KIND_FARM, seller_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"
- ),
- ],
- vec!["p".to_owned(), seller_pubkey],
- vec!["key".to_owned(), "pasture-eggs".to_owned()],
- vec!["title".to_owned(), "Market Eggs".to_owned()],
- vec!["category".to_owned(), "eggs".to_owned()],
- vec!["summary".to_owned(), "Pasture-raised eggs".to_owned()],
- vec!["process".to_owned(), "washed".to_owned()],
- vec!["lot".to_owned(), "lot-a".to_owned()],
- vec!["profile".to_owned(), "dozen".to_owned()],
- vec!["year".to_owned(), "2026".to_owned()],
- vec!["radroots:primary_bin".to_owned(), "bin-1".to_owned()],
- vec![
- "radroots:bin".to_owned(),
- "bin-1".to_owned(),
- "12".to_owned(),
- "each".to_owned(),
- "12".to_owned(),
- "each".to_owned(),
- "dozen".to_owned(),
- ],
- vec![
- "radroots:price".to_owned(),
- "bin-1".to_owned(),
- "6".to_owned(),
- "USD".to_owned(),
- "1".to_owned(),
- "each".to_owned(),
- "6".to_owned(),
- "each".to_owned(),
- ],
- vec!["inventory".to_owned(), "5".to_owned()],
- vec!["status".to_owned(), "active".to_owned()],
- ],
- content: "# Market Eggs".to_owned(),
- sig: "f".repeat(128),
- })
- .expect("seed listing event");
- let executor = SqlxSqliteExecutor::open(Path::new(db_path)).expect("open replica db");
- assert_eq!(
- radroots_replica_ingest_event(&executor, &event).expect("ingest listing"),
- RadrootsReplicaIngestOutcome::Applied
- );
- seed_orderable_listing_signed_event(sandbox, &event, listing_addr);
- event_id
-}
-
-#[allow(dead_code)]
-pub fn seed_market_refresh_provenance(sandbox: &RadrootsCliSandbox, relay_urls: &[&str]) {
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db");
- executor
- .exec(
- "CREATE TABLE IF NOT EXISTS radroots_cli_sync_run (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- scope TEXT NOT NULL,
- relay_set_fingerprint TEXT NOT NULL,
- target_transport_endpoints_json TEXT NOT NULL,
- attempted_transport_endpoints_json TEXT NOT NULL,
- failed_transport_targets_json TEXT NOT NULL,
- started_at INTEGER NOT NULL,
- completed_at INTEGER,
- state TEXT NOT NULL,
- fetched_count INTEGER NOT NULL,
- ingested_count INTEGER NOT NULL,
- skipped_count INTEGER NOT NULL,
- unsupported_count INTEGER NOT NULL,
- failed_count INTEGER NOT NULL,
- failure_reason TEXT
- );
- CREATE INDEX IF NOT EXISTS idx_radroots_cli_sync_run_scope_started
- ON radroots_cli_sync_run(scope, started_at DESC);",
- "[]",
- )
- .expect("create sync run table");
- let relays = relay_urls
- .iter()
- .map(|relay| relay.to_string())
- .collect::<Vec<_>>();
- executor
- .exec(
- "INSERT INTO radroots_cli_sync_run (
- scope,
- relay_set_fingerprint,
- target_transport_endpoints_json,
- attempted_transport_endpoints_json,
- failed_transport_targets_json,
- started_at,
- completed_at,
- state,
- fetched_count,
- ingested_count,
- skipped_count,
- unsupported_count,
- failed_count,
- failure_reason
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14);",
- json!([
- "market_refresh",
- relay_set_fingerprint(&relays),
- serde_json::to_string(&relays).expect("target relays json"),
- serde_json::to_string(&relays).expect("connected relays json"),
- serde_json::to_string(&Vec::<String>::new()).expect("failed relays json"),
- 1_779_000_001_000_i64,
- 1_779_000_001_001_i64,
- "success",
- 1_i64,
- 1_i64,
- 0_i64,
- 0_i64,
- 0_i64,
- Value::Null
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert market refresh provenance");
-}
-
-#[allow(dead_code)]
-fn relay_set_fingerprint(relays: &[String]) -> String {
- let mut normalized = relays
- .iter()
- .map(|relay| relay.trim().to_ascii_lowercase())
- .filter(|relay| !relay.is_empty())
- .collect::<Vec<_>>();
- normalized.sort();
- normalized.dedup();
- let mut hash = 0xcbf29ce484222325_u64;
- for relay in normalized {
- for byte in relay.as_bytes() {
- hash ^= u64::from(*byte);
- hash = hash.wrapping_mul(0x100000001b3);
- }
- hash ^= 0xff;
- hash = hash.wrapping_mul(0x100000001b3);
- }
- format!("relayset_{hash:016x}")
-}
-
-fn seed_orderable_listing_signed_event(
- sandbox: &RadrootsCliSandbox,
- event: &RadrootsEventEnvelope,
- listing_addr: &str,
-) {
- let database_path = sandbox.runtime_store_db_path();
- fs::create_dir_all(database_path.parent().expect("runtime store parent"))
- .expect("runtime store parent");
- let executor = SqlxSqliteExecutor::open(database_path).expect("open runtime store");
- let store = RuntimeStore::new(executor);
- store.migrate_up().expect("migrate runtime store");
- store
- .append_record(&RuntimeStoreRecordInput {
- record_id: format!("test:signed_listing:{}", event.id_str()),
- family: RuntimeStoreRecordFamily::SignedEvent,
- status: RuntimeStoreRecordStatus::Published,
- source_runtime: SourceRuntime::Cli,
- created_at_ms: 1_779_000_001_000,
- inserted_at_ms: 1_779_000_001_000,
- owner_account_id: None,
- owner_pubkey: Some(event.author_str().to_owned()),
- farm_id: None,
- listing_addr: Some(listing_addr.to_owned()),
- local_work_json: None,
- event_id: Some(event.id_str().to_owned()),
- event_kind: Some(i64::from(event.kind_u32())),
- event_pubkey: Some(event.author_str().to_owned()),
- event_created_at: Some(
- i64::try_from(event.created_at_u64()).expect("event created_at"),
- ),
- event_tags_json: Some(json!(event.tags_as_vec())),
- event_content: Some(event.content().to_owned()),
- event_sig: Some(event.sig_str().to_owned()),
- raw_event_json: Some(json!(event)),
- outbox_status: PublishOutboxStatus::Acknowledged,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
- })
- .expect("append listing signed event record");
-}
-
-pub fn remove_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) {
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db");
- let params = serde_json::to_string(&vec![listing_addr]).expect("delete listing params");
- executor
- .exec(
- "DELETE FROM trade_product WHERE listing_addr = ?;",
- params.as_str(),
- )
- .expect("delete listing row");
-}
-
-pub fn update_orderable_listing_available_amount(
- sandbox: &RadrootsCliSandbox,
- listing_addr: &str,
- available_amount: i64,
-) {
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db");
- let params = serde_json::to_string(&serde_json::json!([available_amount, listing_addr]))
- .expect("update listing params");
- executor
- .exec(
- "UPDATE trade_product SET qty_avail = ? WHERE listing_addr = ?;",
- params.as_str(),
- )
- .expect("update listing available amount");
-}
-
-pub fn update_orderable_listing_primary_bin_id(
- sandbox: &RadrootsCliSandbox,
- listing_addr: &str,
- primary_bin_id: Option<&str>,
-) {
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db");
- let params = serde_json::to_string(&serde_json::json!([primary_bin_id, listing_addr]))
- .expect("update listing primary bin params");
- executor
- .exec(
- "UPDATE trade_product SET primary_bin_id = ? WHERE listing_addr = ?;",
- params.as_str(),
- )
- .expect("update listing primary bin");
-}
-
-pub fn duplicate_orderable_listing_row(sandbox: &RadrootsCliSandbox, listing_addr: &str) {
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db");
- let params = serde_json::to_string(&json!([
- "33333333-3333-3333-3333-333333333333",
- listing_addr
- ]))
- .expect("duplicate listing params");
- executor
- .exec(
- "INSERT INTO trade_product (id, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id) SELECT ?, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id FROM trade_product WHERE listing_addr = ?;",
- params.as_str(),
- )
- .expect("duplicate listing row");
-}
-
-pub fn replace_latest_listing_event_id(
- sandbox: &RadrootsCliSandbox,
- listing_addr: &str,
- event_id: &str,
-) {
- let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr);
- let key = format!("{KIND_LISTING}:{seller_pubkey}:{listing_id}");
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db");
- let params = serde_json::to_string(&vec![event_id, key.as_str()]).expect("update params");
- executor
- .exec(
- "UPDATE nostr_event_head SET last_event_id = ? WHERE key = ?;",
- params.as_str(),
- )
- .expect("update latest listing event id");
-}
-
-fn listing_addr_parts(listing_addr: &str) -> (String, String) {
- let parsed = RadrootsListingAddress::parse(listing_addr).expect("listing addr");
- let (_, rest) = parsed.as_str().split_once(':').expect("listing addr kind");
- let (seller_pubkey, listing_id) = rest.split_once(':').expect("listing addr parts");
- (seller_pubkey.to_owned(), listing_id.to_owned())
-}
-
-pub fn create_listing_draft(sandbox: &RadrootsCliSandbox, key: &str) -> PathBuf {
- let accounts = sandbox.json_success(&["--format", "json", "account", "list"]);
- if accounts["result"]["count"].as_u64().unwrap_or_default() == 0 {
- sandbox.json_success(&["--format", "json", "account", "create"]);
- }
- let listing_file = sandbox.root().join(format!("{key}.toml"));
- let listing_file_arg = listing_file.to_string_lossy();
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "create",
- "--output",
- listing_file_arg.as_ref(),
- "--key",
- key,
- "--title",
- "Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- assert_eq!(value["operation_id"], "listing.create");
- listing_file
-}
-
-pub fn identity_public(seed: u8) -> RadrootsIdentityPublic {
- identity_secret(seed).to_public()
-}
-
-pub fn identity_secret(seed: u8) -> RadrootsIdentity {
- let secret = [seed; 32];
- RadrootsIdentity::from_secret_key_bytes(&secret).expect("fixture identity")
-}
-
-pub fn store_test_session_secret(sandbox: &RadrootsCliSandbox, slot: &str, secret: &str) {
- let vault =
- RadrootsProtectedFileSecretVault::new(sandbox.root().join("secrets/shared/accounts"));
- vault
- .store_secret(slot, secret)
- .expect("store test session secret");
-}
-
-pub fn make_listing_publishable(path: &Path, farm_d_tag: &str) {
- let raw = fs::read_to_string(path).expect("listing draft");
- let mut seller_pubkey_present = false;
- let mut in_seller_actor = false;
- let location_city_present = raw
- .lines()
- .any(|line| line.trim_start().starts_with("city ="));
- let location_country_present = raw
- .lines()
- .any(|line| line.trim_start().starts_with("country ="));
- let patched = raw
- .lines()
- .map(|line| {
- let trimmed = line.trim_start();
- if trimmed.starts_with('[') {
- in_seller_actor = trimmed == "[seller_actor]";
- }
- if in_seller_actor && trimmed.starts_with("pubkey =") {
- seller_pubkey_present = !trimmed.ends_with("\"\"");
- line.to_owned()
- } else if trimmed.starts_with("farm_d_tag =") {
- format!("{}farm_d_tag = \"{}\"", line_indent(line), farm_d_tag)
- } else if trimmed.starts_with("method =") {
- format!("{}method = \"pickup\"", line_indent(line))
- } else if trimmed.starts_with("primary =") {
- let mut value = format!("{}primary = \"farmstand\"", line_indent(line));
- if !location_city_present {
- value.push_str(&format!("\n{}city = \"San Francisco\"", line_indent(line)));
- }
- if !location_country_present {
- value.push_str(&format!("\n{}country = \"US\"", line_indent(line)));
- }
- value
- } else if trimmed.starts_with("city =") {
- format!("{}city = \"San Francisco\"", line_indent(line))
- } else if trimmed.starts_with("country =") {
- format!("{}country = \"US\"", line_indent(line))
- } else if trimmed.starts_with("geohash =") {
- format!("{}geohash = \"9q8yy\"", line_indent(line))
- } else {
- line.to_owned()
- }
- })
- .collect::<Vec<_>>()
- .join("\n");
- assert!(seller_pubkey_present, "listing draft seller pubkey");
- fs::write(path, format!("{patched}\n")).expect("write listing draft");
-}
-
-pub fn make_listing_publishable_with_seller(path: &Path, farm_d_tag: &str, seller_pubkey: &str) {
- let raw = fs::read_to_string(path).expect("listing draft");
- let mut seller_pubkey_field_present = false;
- let mut in_seller_actor = false;
- let location_city_present = raw
- .lines()
- .any(|line| line.trim_start().starts_with("city ="));
- let location_country_present = raw
- .lines()
- .any(|line| line.trim_start().starts_with("country ="));
- let patched = raw
- .lines()
- .map(|line| {
- let trimmed = line.trim_start();
- if trimmed.starts_with('[') {
- in_seller_actor = trimmed == "[seller_actor]";
- }
- if in_seller_actor && trimmed.starts_with("pubkey =") {
- seller_pubkey_field_present = true;
- format!("{}pubkey = \"{}\"", line_indent(line), seller_pubkey)
- } else if trimmed.starts_with("farm_d_tag =") {
- format!("{}farm_d_tag = \"{}\"", line_indent(line), farm_d_tag)
- } else if trimmed.starts_with("method =") {
- format!("{}method = \"pickup\"", line_indent(line))
- } else if trimmed.starts_with("primary =") {
- let mut value = format!("{}primary = \"farmstand\"", line_indent(line));
- if !location_city_present {
- value.push_str(&format!("\n{}city = \"San Francisco\"", line_indent(line)));
- }
- if !location_country_present {
- value.push_str(&format!("\n{}country = \"US\"", line_indent(line)));
- }
- value
- } else if trimmed.starts_with("city =") {
- format!("{}city = \"San Francisco\"", line_indent(line))
- } else if trimmed.starts_with("country =") {
- format!("{}country = \"US\"", line_indent(line))
- } else if trimmed.starts_with("geohash =") {
- format!("{}geohash = \"9q8yy\"", line_indent(line))
- } else {
- line.to_owned()
- }
- })
- .collect::<Vec<_>>()
- .join("\n");
- assert!(
- seller_pubkey_field_present,
- "listing draft seller pubkey field"
- );
- fs::write(path, format!("{patched}\n")).expect("write listing draft");
-}
-
-pub fn shell_single_quoted(value: &str) -> String {
- value.replace('\'', "'\"'\"'")
-}
-
-pub fn toml_string(value: &str) -> String {
- value.replace('\\', "\\\\").replace('"', "\\\"")
-}
-
-pub fn write_public_identity_profile(
- sandbox: &RadrootsCliSandbox,
- name: &str,
- identity: &RadrootsIdentityPublic,
-) -> PathBuf {
- let path = sandbox.root().join(format!("{name}.json"));
- fs::write(
- &path,
- serde_json::to_string_pretty(identity).expect("public identity json"),
- )
- .expect("write public identity");
- path
-}
-
-pub fn write_secret_identity_profile(
- sandbox: &RadrootsCliSandbox,
- name: &str,
- identity: &RadrootsIdentity,
-) -> PathBuf {
- let path = sandbox.root().join(format!("{name}.json"));
- identity.save_json(&path).expect("write secret identity");
- path
-}
-
-fn line_indent(line: &str) -> &str {
- let trimmed = line.trim_start();
- &line[..line.len() - trimmed.len()]
-}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -1,8711 +1,144 @@
-mod support;
-
-use std::fs;
-use std::net::{TcpListener, TcpStream};
-use std::path::{Path, PathBuf};
-use std::sync::{
- Arc, Mutex,
- atomic::{AtomicBool, Ordering},
-};
-use std::thread::{self, JoinHandle};
-use std::time::{Duration, Instant};
-
-use nostr::nips::nip44::{self, Version};
-use nostr::{EventBuilder, Keys, Kind, PublicKey, SecretKey, Tag};
-use radroots_event::RadrootsEventPtr;
-use radroots_event::draft::RadrootsEventDraft;
-use radroots_event::ids::{
- RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey,
-};
-use radroots_event::kinds::{KIND_LISTING, KIND_ORDER_REQUEST, KIND_POST};
-use radroots_event::order::{RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRequest};
-use radroots_event_codec::order::order_request_event_build;
-use radroots_identity::RadrootsIdentity;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent, RadrootsNostrKeys, radroots_nostr_build_event,
- radroots_nostr_sign_frozen_draft,
-};
-use radroots_nostr_connect::prelude::{
- RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequest,
- RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
-};
-use radroots_outbox::{
- RadrootsOutbox, RadrootsOutboxDeliveryPlanInput, RadrootsOutboxReticulumBehavior,
- RadrootsOutboxSignedOperationInput,
-};
-use radroots_replica_schema::farm::IFarmFields;
-use radroots_replica_store::{farm, migrations};
-use radroots_replica_sync::radroots_replica_pending_publish_batch;
-use radroots_runtime_store::{
- BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, PublishOutboxStatus, RuntimeStore,
- RuntimeStoreRecordFamily, RuntimeStoreRecordInput, RuntimeStoreRecordStatus, SourceRuntime,
-};
-use radroots_sql_core::SqlxSqliteExecutor;
-use radroots_transport::{
- RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportKind,
- RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget,
-};
+use assert_cmd::Command;
+use radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1;
use serde_json::Value;
-use serde_json::json;
-
-use support::{
- RadrootsCliSandbox, assert_contains, assert_no_daemon_runtime_reference,
- assert_no_removed_command_reference, create_listing_draft, duplicate_orderable_listing_row,
- identity_public, identity_secret, json_from_stdout, make_listing_publishable,
- ndjson_from_stdout, radroots, remove_orderable_listing, replace_latest_listing_event_id,
- seed_market_refresh_provenance, seed_orderable_listing, store_test_session_secret, toml_string,
- update_orderable_listing_available_amount, update_orderable_listing_primary_bin_id,
- write_public_identity_profile, write_secret_identity_profile,
-};
-
-const ORDERABLE_LISTING_RELAY: &str = "wss://relay.example.com";
-
-const FORBIDDEN_FOUNDATION_HARDENING_RETIRED_CONCEPTS: &[(&str, &str)] = &[
- (
- "WireEventParts",
- "event construction must use current Radroots NIP-01 wire part names",
- ),
- (
- "RadrootsFrozenEventDraft",
- "event draft surfaces must use the current RadrootsEventDraft name",
- ),
- (
- "RadrootsNostrEvent",
- "product-level event surfaces must use protocol-neutral event names",
- ),
- (
- "RadrootsNostrEventRef",
- "product-level event references must use RadrootsEventRef",
- ),
- (
- "RadrootsNostrEventPtr",
- "product-level event pointers must use RadrootsEventPtr",
- ),
- (
- "SignedNostrEvent",
- "generic signed-event surfaces must use product-neutral signed-event names",
- ),
- (
- "RadrootsEventIndexIndexCheckpoint",
- "event-index checkpoint names must not duplicate the index noun",
- ),
- (
- "RadrootsEventsIndexed",
- "event-indexed APIs must use the singular event-index crate family",
- ),
- (
- "RADROOTS_EVENTS_VERSION",
- "event contract version constants must use the current singular event namespace",
- ),
- (
- "radroots_events",
- "CLI surfaces must use the current singular event crate names",
- ),
- (
- "radroots_events_codec",
- "CLI event codec surfaces must use the current singular event-codec name",
- ),
- (
- "radroots_events_indexed",
- "CLI event index surfaces must use the current singular event-index name",
- ),
- (
- "radroots_local_events",
- "CLI storage surfaces must not reintroduce retired local-events names",
- ),
- (
- "radroots_local_store",
- "CLI storage surfaces must not reintroduce retired local-store names",
- ),
- (
- "radroots_types",
- "CLI type surfaces must use current crate ownership instead of retired types crates",
- ),
- (
- "radroots_types_bindings",
- "CLI generated surfaces must not reintroduce retired types-binding names",
- ),
- (
- "radroots_nostr_ndb",
- "CLI Nostr database surfaces must not reintroduce retired ndb names",
- ),
- (
- "radroots_replica_db",
- "CLI replica surfaces must use current replica-store ownership",
- ),
- (
- "radroots_replica_db_schema",
- "CLI replica schema surfaces must use current replica-schema ownership",
- ),
- (
- "radroots_sp1_guest_trade",
- "CLI trade SP1 surfaces must use the current trade_sp1 crate names",
- ),
- (
- "radroots_sp1_host_trade",
- "CLI trade SP1 surfaces must use the current trade_sp1 crate names",
- ),
-];
-
-const FORBIDDEN_FOUNDATION_HARDENING_DOC_CONCEPTS: &[(&str, &str)] = &[
- (
- "Nostr event timestamp",
- "CLI docs must describe event-envelope timestamps without generic Nostr wording",
- ),
- (
- "Forwarded satisfies Delivered",
- "CLI docs must not imply forwarded evidence is strict delivery",
- ),
- (
- "StoredByGateway satisfies Delivered",
- "CLI docs must not imply gateway storage evidence is strict delivery",
- ),
- (
- "Seen satisfies Delivered",
- "CLI docs must not imply seen evidence is strict delivery",
- ),
-];
-
-const LISTING_ADDR: &str =
- "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-const DERIVED_PROJECTION_SYNC_PUSH_FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
-
-fn test_order_id(value: &str) -> RadrootsOrderId {
- value.parse().expect("valid order id")
-}
-
-fn test_listing_addr(value: &str) -> RadrootsListingAddress {
- value.parse().expect("valid listing address")
-}
-
-fn test_inventory_bin_id(value: &str) -> RadrootsInventoryBinId {
- value.parse().expect("valid inventory bin id")
-}
-
-fn test_pubkey(value: &str) -> RadrootsPublicKey {
- value.parse().expect("valid public key")
-}
-
-fn configure_nostr_transport(sandbox: &RadrootsCliSandbox, relay_url: &str) {
- sandbox.write_nostr_transport_profile(&[relay_url]);
-}
-
-fn assert_no_removed_transport_status_fields(status: &Value) {
- for field in [
- "transport_kind",
- "implementation_state",
- "readiness",
- "publish_usable",
- "fetch_usable",
- "redacted_message",
- ] {
- assert!(
- status.get(field).is_none(),
- "{field} must not be serialized"
- );
- }
-}
-
-fn assert_transport_capabilities(status: &Value, deliver: bool, fetch: bool) {
- assert_eq!(status["capabilities"]["deliver"], deliver);
- assert_eq!(status["capabilities"]["fetch"], fetch);
-}
-
-fn sdk_outbox_path(sandbox: &RadrootsCliSandbox) -> PathBuf {
- sandbox
- .root()
- .join("data/apps/cli/replica/sdk/runtime.sqlite")
-}
-
-fn post_outbox_draft(public_key_hex: &str, content: &str) -> RadrootsEventDraft {
- RadrootsEventDraft::new(
- "radroots.social.post.v1",
- KIND_POST,
- 1_700_000_000,
- vec![vec!["t".to_owned(), "soil".to_owned()]],
- content,
- public_key_hex,
- )
- .expect("post outbox draft")
-}
-
-fn outbox_signer(seed: u8) -> (String, RadrootsNostrKeys) {
- let identity = identity_secret(seed);
- let public_key_hex = identity.public_key_hex();
- (public_key_hex, identity.into_keys())
-}
-
-fn transport_target(kind: RadrootsTransportKind, uri: &str) -> RadrootsTransportTarget {
- RadrootsTransportTarget::new(kind, uri).expect("transport target")
-}
-
-fn seed_signed_transport_outbox_event(
- sandbox: &RadrootsCliSandbox,
- content: &str,
- transport_profile_id: &str,
- target: RadrootsTransportTarget,
- behavior: RadrootsOutboxReticulumBehavior,
- claim_for_publish: bool,
-) {
- let outbox_path = sdk_outbox_path(sandbox);
- fs::create_dir_all(outbox_path.parent().expect("outbox parent")).expect("outbox parent");
- let runtime = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()
- .expect("outbox fixture runtime");
- runtime.block_on(async {
- let outbox = RadrootsOutbox::open_file(&outbox_path)
- .await
- .expect("open outbox fixture");
- let (public_key_hex, keys) = outbox_signer(201);
- let draft = post_outbox_draft(public_key_hex.as_str(), content);
- let signed_event =
- radroots_nostr_sign_frozen_draft(&keys, &draft).expect("signed outbox event");
- let receipt = outbox
- .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new(
- "transport_status_fixture",
- draft,
- signed_event,
- RadrootsOutboxDeliveryPlanInput::new(
- transport_profile_id,
- 1,
- RadrootsTransportSatisfactionPolicy::all_accepted(),
- vec![target],
- )
- .with_reticulum_behavior(behavior),
- true,
- 1_700_000_000_007,
- 1_700_000_000_000,
- ))
- .await
- .expect("enqueue outbox fixture");
- if claim_for_publish {
- outbox
- .claim_next_ready_signed_event(
- "transport_status_fixture",
- "claim-publishing",
- 1_700_000_010_000,
- 1_700_000_000_100,
- )
- .await
- .expect("claim outbox fixture")
- .expect("claimed outbox fixture");
- let event = outbox
- .get_event(receipt.outbox_event_id)
- .await
- .expect("read claimed event")
- .expect("claimed event");
- assert_eq!(event.state.as_str(), "publishing");
- }
- });
-}
-
-fn assert_terminal_field(stdout: &str, label: &str, expected_value: &str) {
- assert!(
- stdout.lines().any(|line| {
- let trimmed = line.trim_start();
- trimmed.starts_with(label) && trimmed.trim_end().ends_with(expected_value)
- }),
- "missing terminal field `{label}` with value `{expected_value}` in `{stdout}`"
- );
-}
-
-struct RelayFetchServer {
- endpoint: String,
- handle: JoinHandle<()>,
-}
-
-impl RelayFetchServer {
- fn with_events(events: Vec<RadrootsNostrEvent>) -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind relay fetch");
- let endpoint = format!("ws://{}", listener.local_addr().expect("relay fetch addr"));
- let handle = thread::spawn(move || {
- let (stream, _) = listener.accept().expect("accept relay fetch connection");
- handle_relay_fetch_connection(stream, events);
- });
- Self { endpoint, handle }
- }
-
- fn endpoint(&self) -> &str {
- self.endpoint.as_str()
- }
-
- fn join(self) {
- self.handle.join().expect("relay fetch server join");
- }
-}
-
-struct Nip46RelayReport {
- connection_count: usize,
- req_count: usize,
- sign_request_count: usize,
- published_events: Vec<RadrootsNostrEvent>,
-}
-struct Nip46RelayState {
- connection_count: Mutex<usize>,
- req_count: Mutex<usize>,
- sign_request_count: Mutex<usize>,
- published_events: Mutex<Vec<RadrootsNostrEvent>>,
- pending_responses: Mutex<Vec<RadrootsNostrEvent>>,
- done: AtomicBool,
-}
-
-struct Nip46RelayServer {
- endpoint: String,
- state: Arc<Nip46RelayState>,
- handle: JoinHandle<()>,
-}
-
-impl Nip46RelayServer {
- fn new(remote_keys: Keys, user_keys: Keys) -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind nip46 relay");
- listener.set_nonblocking(true).expect("nip46 nonblocking");
- let endpoint = format!("ws://{}", listener.local_addr().expect("nip46 addr"));
- let state = Arc::new(Nip46RelayState {
- connection_count: Mutex::new(0),
- req_count: Mutex::new(0),
- sign_request_count: Mutex::new(0),
- published_events: Mutex::new(Vec::new()),
- pending_responses: Mutex::new(Vec::new()),
- done: AtomicBool::new(false),
- });
- let thread_state = Arc::clone(&state);
- let remote_keys = Arc::new(remote_keys);
- let user_keys = Arc::new(user_keys);
- let handle = thread::spawn(move || {
- let deadline = Instant::now() + Duration::from_secs(10);
- while Instant::now() < deadline && !thread_state.done.load(Ordering::SeqCst) {
- match listener.accept() {
- Ok((stream, _)) => {
- *thread_state
- .connection_count
- .lock()
- .expect("connection count lock") += 1;
- let connection_state = Arc::clone(&thread_state);
- let remote_keys = Arc::clone(&remote_keys);
- let user_keys = Arc::clone(&user_keys);
- thread::spawn(move || {
- handle_nip46_relay_connection(
- stream,
- (*remote_keys).clone(),
- (*user_keys).clone(),
- connection_state,
- );
- });
- }
- Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
- thread::sleep(Duration::from_millis(10));
- }
- Err(error) => panic!("accept nip46 relay connection: {error}"),
- }
- }
- assert!(
- thread_state.done.load(Ordering::SeqCst),
- "timed out waiting for NIP-46 relay proof; connections {}; req {}; sign requests {}; published events {}",
- *thread_state
- .connection_count
- .lock()
- .expect("connection count lock"),
- *thread_state.req_count.lock().expect("req count lock"),
- *thread_state
- .sign_request_count
- .lock()
- .expect("sign count lock"),
- thread_state
- .published_events
- .lock()
- .expect("published events lock")
- .len(),
- );
- });
- Self {
- endpoint,
- state,
- handle,
- }
- }
-
- fn endpoint(&self) -> &str {
- self.endpoint.as_str()
- }
-
- fn join(self) -> Nip46RelayReport {
- self.handle.join().expect("nip46 relay server join");
- Nip46RelayReport {
- connection_count: *self
- .state
- .connection_count
- .lock()
- .expect("connection count lock"),
- req_count: *self.state.req_count.lock().expect("req count lock"),
- sign_request_count: *self
- .state
- .sign_request_count
- .lock()
- .expect("sign count lock"),
- published_events: self
- .state
- .published_events
- .lock()
- .expect("published events lock")
- .clone(),
- }
- }
-}
-
-fn handle_nip46_relay_connection(
- stream: TcpStream,
- remote_keys: Keys,
- user_keys: Keys,
- state: Arc<Nip46RelayState>,
-) {
- stream
- .set_nonblocking(false)
- .expect("nip46 blocking stream");
- let mut websocket = tungstenite::accept(stream).expect("accept nip46 websocket");
- let mut subscriptions = Vec::<String>::new();
- loop {
- let message = match websocket.read() {
- Ok(message) => message,
- Err(_) => return,
- };
- if !message.is_text() {
- continue;
- }
- let value: Value =
- serde_json::from_str(message.to_text().expect("nip46 text")).expect("nip46 json");
- match value.get(0).and_then(Value::as_str) {
- Some("REQ") => {
- *state.req_count.lock().expect("req count lock") += 1;
- let subscription_id = value
- .get(1)
- .and_then(Value::as_str)
- .expect("nip46 subscription id")
- .to_owned();
- subscriptions.push(subscription_id.clone());
- websocket
- .send(tungstenite::Message::Text(
- json!(["EOSE", subscription_id]).to_string().into(),
- ))
- .expect("send nip46 eose");
- send_pending_nip46_responses(&mut websocket, subscription_id.as_str(), &state);
- }
- Some("CLOSE") => {}
- Some("EVENT") => {
- let event: RadrootsNostrEvent =
- serde_json::from_value(value.get(1).cloned().expect("nip46 relay event"))
- .expect("parse nip46 relay event");
- if event.kind == Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
- handle_nip46_sign_request(
- &mut websocket,
- &subscriptions,
- event,
- &remote_keys,
- &user_keys,
- &state,
- );
- } else {
- handle_nip46_product_publish(&mut websocket, event, &state);
- }
- }
- _ => {}
- }
- }
-}
+const UUID_V7: &str = "01890f78-9abc-7def-8abc-123456789abc";
-fn handle_nip46_sign_request(
- websocket: &mut tungstenite::WebSocket<TcpStream>,
- subscriptions: &[String],
- event: RadrootsNostrEvent,
- remote_keys: &Keys,
- user_keys: &Keys,
- state: &Nip46RelayState,
-) {
- send_relay_ok(websocket, &event);
- let decrypted = nip44::decrypt(remote_keys.secret_key(), &event.pubkey, &event.content)
- .expect("decrypt nip46 request");
- let request: RadrootsNostrConnectRequestMessage =
- serde_json::from_str(&decrypted).expect("decode nip46 request");
- let response = match request.request {
- RadrootsNostrConnectRequest::SignEvent(unsigned_event) => {
- let signed = unsigned_event
- .sign_with_keys(user_keys)
- .expect("sign nip46 request");
- RadrootsNostrConnectResponse::SignedEvent(signed)
- }
- other => RadrootsNostrConnectResponse::Error {
- result: None,
- error: format!("unexpected test NIP-46 method `{}`", other.method()),
- },
- };
- let response_event =
- nip46_response_event(remote_keys, event.pubkey, request.id.as_str(), response);
- state
- .pending_responses
- .lock()
- .expect("pending response lock")
- .push(response_event.clone());
- for subscription_id in subscriptions {
- send_nip46_response(websocket, subscription_id.as_str(), &response_event);
- }
- *state
- .sign_request_count
- .lock()
- .expect("sign request count lock") += 1;
+fn radroots() -> Command {
+ Command::cargo_bin("radroots").expect("radroots binary")
}
-fn send_pending_nip46_responses(
- websocket: &mut tungstenite::WebSocket<TcpStream>,
- subscription_id: &str,
- state: &Nip46RelayState,
-) {
- let responses = state
- .pending_responses
- .lock()
- .expect("pending response lock")
+fn json_output(args: &[&str]) -> Value {
+ let output = radroots()
+ .args(args)
+ .assert()
+ .success()
+ .get_output()
.clone();
- for response in responses {
- send_nip46_response(websocket, subscription_id, &response);
- }
+ serde_json::from_slice(&output.stdout).expect("json stdout")
}
-fn send_nip46_response(
- websocket: &mut tungstenite::WebSocket<TcpStream>,
- subscription_id: &str,
- response_event: &RadrootsNostrEvent,
-) {
- websocket
- .send(tungstenite::Message::Text(
- json!(["EVENT", subscription_id, response_event])
- .to_string()
- .into(),
- ))
- .expect("send nip46 response event");
-}
-
-fn handle_nip46_product_publish(
- websocket: &mut tungstenite::WebSocket<TcpStream>,
- event: RadrootsNostrEvent,
- state: &Nip46RelayState,
-) {
- send_relay_ok(websocket, &event);
- state
- .published_events
- .lock()
- .expect("published events lock")
- .push(event);
- state.done.store(true, Ordering::SeqCst);
-}
-
-fn nip46_response_event(
- remote_keys: &Keys,
- client_public_key: PublicKey,
- request_id: &str,
- response: RadrootsNostrConnectResponse,
-) -> RadrootsNostrEvent {
- let envelope = response
- .into_envelope(request_id)
- .expect("nip46 response envelope");
- let payload = serde_json::to_string(&envelope).expect("nip46 response payload");
- let ciphertext = nip44::encrypt(
- remote_keys.secret_key(),
- &client_public_key,
- payload,
- Version::V2,
- )
- .expect("nip46 response ciphertext");
- EventBuilder::new(Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), ciphertext)
- .tag(Tag::public_key(client_public_key))
- .sign_with_keys(remote_keys)
- .expect("nip46 response event")
-}
-
-fn send_relay_ok(websocket: &mut tungstenite::WebSocket<TcpStream>, event: &RadrootsNostrEvent) {
- websocket
- .send(tungstenite::Message::Text(
- json!(["OK", event.id.to_hex(), true, ""])
- .to_string()
- .into(),
- ))
- .expect("send relay ok");
-}
-
-fn nostr_keys_from_identity(identity: &RadrootsIdentity) -> Keys {
- let secret_key_hex = identity.secret_key_hex();
- Keys::new(SecretKey::from_hex(secret_key_hex.as_str()).expect("secret key"))
-}
-
-fn myc_nip46_config(
- remote_signer_pubkey: &str,
- relay_endpoint: &str,
- managed_account_ref: &str,
- session_ref: &str,
-) -> String {
- format!(
- r#"[signer]
-backend = "myc"
-
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "explicit_endpoint"
-target = "bunker://{}?relay={}"
-managed_account_ref = "{}"
-signer_session_ref = "{}"
-"#,
- toml_string(remote_signer_pubkey),
- toml_string(relay_endpoint),
- toml_string(managed_account_ref),
- toml_string(session_ref),
- )
-}
-
-fn handle_relay_fetch_connection(stream: TcpStream, events: Vec<RadrootsNostrEvent>) {
- let mut websocket = tungstenite::accept(stream).expect("accept fetch websocket");
- let subscription_id = read_relay_req_subscription_id(&mut websocket);
- for event in events {
- websocket
- .send(tungstenite::Message::Text(
- json!(["EVENT", subscription_id, event]).to_string().into(),
- ))
- .expect("relay event send");
- }
- websocket
- .send(tungstenite::Message::Text(
- json!(["EOSE", subscription_id]).to_string().into(),
- ))
- .expect("relay eose send");
-}
-
-fn read_relay_req_subscription_id(websocket: &mut tungstenite::WebSocket<TcpStream>) -> String {
- loop {
- let message = websocket.read().expect("relay req message");
- if !message.is_text() {
- continue;
- }
- let value: Value =
- serde_json::from_str(message.to_text().expect("relay req text")).expect("relay json");
- if value.get(0).and_then(Value::as_str) == Some("REQ") {
- return value
- .get(1)
- .and_then(Value::as_str)
- .expect("subscription id")
- .to_owned();
- }
- }
-}
-
-fn seed_derived_projection_sync_farm(sandbox: &RadrootsCliSandbox, d_tag: &str, pubkey: &str) {
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica");
- migrations::run_all_up(&executor).expect("replica migrations");
- farm::create(
- &executor,
- &IFarmFields {
- d_tag: d_tag.to_owned(),
- pubkey: pubkey.to_owned(),
- name: "Sync Push Farm".to_owned(),
- about: Some("sync push process fixture".to_owned()),
- website: None,
- picture: None,
- banner: None,
- location_primary: None,
- location_city: None,
- location_region: None,
- location_country: None,
- },
- )
- .expect("seed derived projection sync farm");
-}
-
-fn seed_app_farm_record(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- seller_pubkey: &str,
- farm_d_tag: &str,
-) {
- append_app_local_record(
- RuntimeStoreRecordInput {
- record_id: format!("app:local_work:farm:{farm_d_tag}:test"),
- family: RuntimeStoreRecordFamily::LocalWork,
- status: RuntimeStoreRecordStatus::LocalSaved,
- source_runtime: SourceRuntime::App,
- created_at_ms: 1_779_000_001_000,
- inserted_at_ms: 1_779_000_001_000,
- owner_account_id: Some(account_id.to_owned()),
- owner_pubkey: Some(seller_pubkey.to_owned()),
- farm_id: Some(farm_d_tag.to_owned()),
- listing_addr: None,
- local_work_json: Some(json!({
- "record_kind": "farm_config_v1",
- "scope": "app",
- "document": {
- "version": 1,
- "selection": {
- "scope": "app",
- "account": account_id,
- "farm_d_tag": farm_d_tag,
- },
- "profile": {
- "name": "App Farm",
- "display_name": "App Farm",
- },
- "farm": {
- "d_tag": farm_d_tag,
- "name": "App Farm",
- "location": {
- "primary": "farmstand",
- "city": "San Francisco",
- "country": "US",
- "geohash": "9q8yy",
- },
- },
- "listing_defaults": {
- "delivery_method": "pickup",
- "location": {
- "primary": "farmstand",
- "city": "San Francisco",
- "country": "US",
- "geohash": "9q8yy",
- },
- },
- },
- })),
- event_id: None,
- event_kind: None,
- event_pubkey: None,
- event_created_at: None,
- event_tags_json: None,
- event_content: None,
- event_sig: None,
- raw_event_json: None,
- outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
- },
- sandbox,
- );
-}
-
-fn seed_app_listing_record(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- seller_pubkey: &str,
- farm_d_tag: &str,
- listing_d_tag: &str,
-) -> String {
- seed_app_listing_record_variant(
- sandbox,
- account_id,
- Some(seller_pubkey),
- farm_d_tag,
- listing_d_tag,
- "test",
- "App Eggs",
- None,
- )
-}
-
-fn seed_app_listing_record_variant(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- seller_pubkey: Option<&str>,
- farm_d_tag: &str,
- listing_d_tag: &str,
- record_suffix: &str,
- title: &str,
- exportability: Option<serde_json::Value>,
-) -> String {
- seed_app_listing_record_variant_with_listing_addr(
- sandbox,
- account_id,
- seller_pubkey,
- farm_d_tag,
- listing_d_tag,
- record_suffix,
- title,
- exportability,
- true,
- )
-}
-
-fn seed_app_listing_record_variant_without_listing_addr(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- seller_pubkey: Option<&str>,
- farm_d_tag: &str,
- listing_d_tag: &str,
- record_suffix: &str,
- title: &str,
-) -> String {
- seed_app_listing_record_variant_with_listing_addr(
- sandbox,
- account_id,
- seller_pubkey,
- farm_d_tag,
- listing_d_tag,
- record_suffix,
- title,
- None,
- false,
- )
-}
-
-fn seed_app_listing_record_variant_with_listing_addr(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- seller_pubkey: Option<&str>,
- farm_d_tag: &str,
- listing_d_tag: &str,
- record_suffix: &str,
- title: &str,
- exportability: Option<serde_json::Value>,
- include_listing_addr: bool,
-) -> String {
- seed_app_listing_record_identity_variant(
- sandbox,
- account_id,
- seller_pubkey,
- seller_pubkey,
- farm_d_tag,
- listing_d_tag,
- record_suffix,
- title,
- exportability,
- include_listing_addr,
- )
-}
-
-fn seed_app_listing_record_identity_variant(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- document_seller_pubkey: Option<&str>,
- owner_pubkey: Option<&str>,
- farm_d_tag: &str,
- listing_d_tag: &str,
- record_suffix: &str,
- title: &str,
- exportability: Option<serde_json::Value>,
- include_listing_addr: bool,
-) -> String {
- let record_id = format!("app:local_work:listing:{listing_d_tag}:{record_suffix}");
- let seller_pubkey_json = document_seller_pubkey
- .map(|value| json!(value))
- .unwrap_or_else(|| json!(null));
- let mut payload = json!({
- "record_kind": "listing_draft_v1",
- "document": {
- "version": 1,
- "kind": "listing_draft_v1",
- "listing": {
- "d_tag": listing_d_tag,
- "farm_d_tag": farm_d_tag,
- },
- "seller_actor": {
- "account_id": account_id,
- "pubkey": seller_pubkey_json,
- "source": "farm_config",
- },
- "product": {
- "key": listing_d_tag,
- "title": title,
- "category": "eggs",
- "summary": "Fresh app eggs",
- },
- "primary_bin": {
- "bin_id": "bin-1",
- "quantity_amount": "1",
- "quantity_unit": "dozen",
- "price_amount": "7.50",
- "price_currency": "USD",
- "price_per_amount": "1",
- "price_per_unit": "dozen",
- },
- "inventory": {
- "available": "12",
- },
- "availability": {
- "kind": "local",
- "status": "draft",
- },
- "delivery": {
- "method": "pickup",
- },
- "location": {
- "primary": "farmstand",
- "city": "San Francisco",
- "country": "US",
- "geohash": "9q8yy",
- },
- },
- });
- if let Some(exportability) = exportability {
- payload["exportability"] = exportability;
- }
- append_app_local_record(
- RuntimeStoreRecordInput {
- record_id: record_id.clone(),
- family: RuntimeStoreRecordFamily::LocalWork,
- status: RuntimeStoreRecordStatus::LocalSaved,
- source_runtime: SourceRuntime::App,
- created_at_ms: 1_779_000_002_000,
- inserted_at_ms: 1_779_000_002_000,
- owner_account_id: Some(account_id.to_owned()),
- owner_pubkey: owner_pubkey.map(str::to_owned),
- farm_id: Some(farm_d_tag.to_owned()),
- listing_addr: include_listing_addr
- .then_some(owner_pubkey)
- .flatten()
- .map(|owner_pubkey| format!("30402:{owner_pubkey}:{listing_d_tag}")),
- local_work_json: Some(payload),
- event_id: None,
- event_kind: None,
- event_pubkey: None,
- event_created_at: None,
- event_tags_json: None,
- event_content: None,
- event_sig: None,
- raw_event_json: None,
- outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
- },
- sandbox,
- );
- record_id
-}
-
-fn append_app_local_record(input: RuntimeStoreRecordInput, sandbox: &RadrootsCliSandbox) {
- let database_path = sandbox.runtime_store_db_path();
- fs::create_dir_all(database_path.parent().expect("runtime store parent"))
- .expect("runtime store parent");
- let executor = SqlxSqliteExecutor::open(database_path).expect("open runtime store");
- let store = RuntimeStore::new(executor);
- store.migrate_up().expect("migrate runtime store");
- store
- .append_record(&input)
- .expect("append app local event record");
-}
-
-fn seed_app_order_record(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- order_id: &str,
- listing_addr: &str,
- listing_event_id: &str,
-) -> String {
- seed_app_order_record_variant(
- sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey,
- order_id,
- listing_addr,
- listing_event_id,
- true,
- "supported",
- Vec::new(),
- )
-}
-
-fn seed_app_order_record_variant(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- order_id: &str,
- listing_addr: &str,
- listing_event_id: &str,
- current: bool,
- support_state: &str,
- support_issues: Vec<&str>,
-) -> String {
- let record_id = format!("app:local_work:order_request:{order_id}");
- seed_app_order_record_variant_with_record_id(
- sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey,
- order_id,
- listing_addr,
- listing_event_id,
- record_id,
- current,
- support_state,
- support_issues,
- )
-}
-
-fn seed_app_order_record_variant_with_record_id(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- order_id: &str,
- listing_addr: &str,
- listing_event_id: &str,
- record_id: String,
- current: bool,
- support_state: &str,
- support_issues: Vec<&str>,
-) -> String {
- let support_issues = support_issues
- .into_iter()
- .map(|issue| Value::String(issue.to_owned()))
- .collect::<Vec<_>>();
- let payload = json!({
- "record_kind": BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND,
- "scope": "app",
- "exportability": {
- "state": "exportable",
- },
- "support_status": {
- "state": support_state,
- "issues": support_issues,
- },
- "currentness": {
- "current": current,
- "source": "app_sqlite_order",
- "record_id": record_id,
- "order_id": order_id,
- "order_updated_at": "2026-05-24T12:00:10Z",
- "created_at_ms": 1_779_000_010_000_i64,
- },
- "document": {
- "version": 1,
- "kind": "order_draft_v1",
- "order": {
- "order_id": order_id,
- "listing_addr": listing_addr,
- "listing_event_id": listing_event_id,
- "listing_relays": [ORDERABLE_LISTING_RELAY],
- "buyer_pubkey": buyer_pubkey,
- "seller_pubkey": seller_pubkey,
- "items": [
- {
- "bin_id": "bin-1",
- "bin_count": 2,
- }
- ],
- "economics": {
- "quote_id": format!("app-order:{order_id}"),
- "quote_version": 1,
- "pricing_basis": "listing_event",
- "currency": "USD",
- "items": [
- {
- "bin_id": "bin-1",
- "bin_count": 2,
- "quantity_amount": "1",
- "quantity_unit": "each",
- "unit_price_amount": "6",
- "unit_price_currency": "USD",
- "line_subtotal": {
- "amount": "12",
- "currency": "USD",
- },
- }
- ],
- "discounts": [],
- "adjustments": [],
- "subtotal": {
- "amount": "12",
- "currency": "USD",
- },
- "discount_total": {
- "amount": "0",
- "currency": "USD",
- },
- "adjustment_total": {
- "amount": "0",
- "currency": "USD",
- },
- "total": {
- "amount": "12",
- "currency": "USD",
- },
- },
- },
- "buyer_actor": {
- "account_id": account_id,
- "pubkey": buyer_pubkey,
- "source": "resolved_account",
- },
- "listing_lookup": listing_addr,
- },
- "app_order": {
- "order_id": order_id,
- "order_number": 1,
- "farm_id": "018f47a8-7b2c-7000-8000-0000000000f1",
- "farm_display_name": "CLI Interop Farm",
- "farm_key": "pasture-eggs",
- "status": "placed",
- "buyer_context_key": "buyer_context",
- "lines": [
- {
- "line_id": format!("{order_id}:product-eggs"),
- "product_id": "product-eggs",
- "listing_addr": listing_addr,
- "listing_event_id": listing_event_id,
- "seller_pubkey": seller_pubkey,
- }
- ],
- },
- });
- append_app_local_record(
- RuntimeStoreRecordInput {
- record_id: record_id.clone(),
- family: RuntimeStoreRecordFamily::LocalWork,
- status: RuntimeStoreRecordStatus::LocalSaved,
- source_runtime: SourceRuntime::App,
- created_at_ms: 1_779_000_010_000,
- inserted_at_ms: 1_779_000_010_000,
- owner_account_id: Some(account_id.to_owned()),
- owner_pubkey: Some(buyer_pubkey.to_owned()),
- farm_id: Some("018f47a8-7b2c-7000-8000-0000000000f1".to_owned()),
- listing_addr: Some(listing_addr.to_owned()),
- local_work_json: Some(payload),
- event_id: None,
- event_kind: None,
- event_pubkey: None,
- event_created_at: None,
- event_tags_json: None,
- event_content: None,
- event_sig: None,
- raw_event_json: None,
- outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
- },
- sandbox,
- );
- record_id
-}
-
-fn app_order_economics(order_id: &str, bin_count: u32) -> RadrootsOrderEconomics {
- let line_total = (bin_count * 6).to_string();
- serde_json::from_value(json!({
- "quote_id": format!("app-order:{order_id}"),
- "quote_version": 1,
- "pricing_basis": "listing_event",
- "currency": "USD",
- "items": [
- {
- "bin_id": "bin-1",
- "bin_count": bin_count,
- "quantity_amount": "1",
- "quantity_unit": "each",
- "unit_price_amount": "6",
- "unit_price_currency": "USD",
- "line_subtotal": {
- "amount": line_total,
- "currency": "USD",
- },
- }
- ],
- "discounts": [],
- "adjustments": [],
- "subtotal": {
- "amount": line_total,
- "currency": "USD",
- },
- "discount_total": {
- "amount": "0",
- "currency": "USD",
- },
- "adjustment_total": {
- "amount": "0",
- "currency": "USD",
- },
- "total": {
- "amount": line_total,
- "currency": "USD",
- },
- }))
- .expect("app order economics")
+fn json_failure(args: &[&str]) -> Value {
+ let output = radroots()
+ .args(args)
+ .assert()
+ .failure()
+ .get_output()
+ .clone();
+ serde_json::from_slice(&output.stdout).expect("json failure stdout")
}
-fn signed_app_order_request_event(
- buyer: &radroots_identity::RadrootsIdentity,
- order_id: &str,
- listing_addr: &str,
- listing_event_id: &str,
- seller_pubkey: &str,
- bin_count: u32,
-) -> RadrootsNostrEvent {
- let payload = RadrootsOrderRequest {
- order_id: test_order_id(order_id),
- listing_addr: test_listing_addr(listing_addr),
- buyer_pubkey: test_pubkey(buyer.public_key_hex().as_str()),
- seller_pubkey: test_pubkey(seller_pubkey),
- items: vec![RadrootsOrderItem {
- bin_id: test_inventory_bin_id("bin-1"),
- bin_count,
- }],
- economics: app_order_economics(order_id, bin_count),
- };
- let parts = order_request_event_build(
- &RadrootsEventPtr {
- id: listing_event_id.to_owned(),
- relays: None,
- },
- &payload,
- )
- .expect("app order request parts");
- radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("nostr event builder")
- .sign_with_keys(buyer.keys())
- .expect("signed app order request")
-}
+#[test]
+fn generated_v1_read_operation_runs_from_binary() {
+ let value = json_output(&["--format", "json", "profile", "inspect"]);
-fn append_app_signed_order_request_record(
- sandbox: &RadrootsCliSandbox,
- account_id: &str,
- listing_addr: &str,
- event: &RadrootsNostrEvent,
-) -> String {
- let event_id = event.id.to_hex();
- let event_tags = event
- .tags
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect::<Vec<_>>();
- let record_id = format!("app:signed_event:{event_id}");
- append_app_local_record(
- RuntimeStoreRecordInput {
- record_id: record_id.clone(),
- family: RuntimeStoreRecordFamily::SignedEvent,
- status: RuntimeStoreRecordStatus::Published,
- source_runtime: SourceRuntime::App,
- created_at_ms: i64::try_from(event.created_at.as_secs()).expect("event created_at")
- * 1_000,
- inserted_at_ms: 1_779_000_011_000,
- owner_account_id: Some(account_id.to_owned()),
- owner_pubkey: Some(event.pubkey.to_string()),
- farm_id: Some("018f47a8-7b2c-7000-8000-0000000000f1".to_owned()),
- listing_addr: Some(listing_addr.to_owned()),
- local_work_json: None,
- event_id: Some(event_id),
- event_kind: Some(i64::from(KIND_ORDER_REQUEST)),
- event_pubkey: Some(event.pubkey.to_string()),
- event_created_at: Some(
- i64::try_from(event.created_at.as_secs()).expect("event created_at"),
- ),
- event_tags_json: Some(json!(event_tags.clone())),
- event_content: Some(event.content.clone()),
- event_sig: Some(event.sig.to_string()),
- raw_event_json: Some(json!({
- "id": event.id.to_hex(),
- "pubkey": event.pubkey.to_string(),
- "created_at": i64::try_from(event.created_at.as_secs()).expect("event created_at"),
- "kind": u32::from(event.kind.as_u16()),
- "tags": event_tags,
- "content": event.content.clone(),
- "sig": event.sig.to_string(),
- })),
- outbox_status: PublishOutboxStatus::Acknowledged,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
- },
- sandbox,
- );
- record_id
+ assert_eq!(value["operation_id"], "profile.inspect");
+ assert_eq!(value["kind"], "profile.inspect");
+ assert_eq!(value["status"], "ok");
}
#[test]
-fn root_help_exposes_only_target_namespaces() {
- let output = radroots().arg("--help").output().expect("run root help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- for namespace in [
- "workspace",
- "health",
- "config",
- "account",
- "signer",
- "transport",
- "mesh",
- "store",
- "sync",
- "farm",
- "listing",
- "market",
- "basket",
- "trade",
- "validation",
- ] {
- assert!(
- help_lists(&stdout, namespace),
- "root help should contain `{namespace}`"
- );
- }
-
- for removed in [
- "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "relay",
- "product", "runtime", "job", "message", "approval", "agent",
+fn binary_rejects_retired_command_surfaces() {
+ for args in [
+ &["workspace", "get"][..],
+ &["config", "get"],
+ &["mesh", "status"],
+ &["trade", "submit"],
+ &["listing", "archive"],
+ &["validation", "receipt", "list"],
] {
- assert!(
- !help_lists(&stdout, removed),
- "root help should not contain `{removed}`"
- );
+ radroots().args(args).assert().failure();
}
}
#[test]
-fn root_help_explains_transport_and_mesh_surfaces() {
- let output = radroots().arg("--help").output().expect("run root help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
-
- assert!(stdout.contains("Manage transport profiles and outbox delivery."));
- assert!(stdout.contains("Inspect mesh scope and Reticulum admission policy."));
- assert!(stdout.contains("Inspect local readiness and mode-specific recovery steps"));
- assert!(stdout.contains("Show effective configuration and publish-plane readiness."));
-}
-
-#[test]
-fn transport_profile_reticulum_output_is_transport_specific() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config(
- "[transport]\nprofile = \"reticulum\"\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n",
- );
-
- let value = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]);
- let result = &value["result"];
- let actions = result["actions"].as_array().expect("actions");
-
- assert_eq!(value["operation_id"], "transport.profile.get");
- assert_eq!(result["profile_id"], "reticulum");
- assert_eq!(result["profile_kind"], "reticulum");
- assert_eq!(result["profile_delivery_usable"], false);
- assert!(result.get("transport_kind").is_none());
- assert!(result.get("implementation_state").is_none());
- assert_eq!(result["transport_statuses"][0]["transport"], "reticulum");
- assert_eq!(result["transport_statuses"][0]["implementation"], "real");
- assert_eq!(result["transport_statuses"][0]["maturity"], "preview");
- assert_eq!(
- result["transport_statuses"][0]["availability"],
- "unavailable"
- );
- assert_eq!(result["transport_statuses"][0]["configured"], true);
- assert_eq!(
- result["transport_statuses"][0]["usable_for_delivery"],
- false
- );
- assert_transport_capabilities(&result["transport_statuses"][0], false, false);
- assert_eq!(
- result["transport_statuses"][0]["message"],
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
- );
- assert_no_removed_transport_status_fields(&result["transport_statuses"][0]);
- assert_eq!(result["reticulum_behavior"], "defer_delivery_plans");
- assert_eq!(result["reticulum_scope"], "local");
- assert_eq!(result["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE);
- assert!(
- !result["message"]
- .as_str()
- .expect("message")
- .contains("Nostr")
- );
- assert_eq!(actions.len(), 2);
- assert_eq!(actions[0], "radroots mesh status");
- assert_eq!(actions[1], "radroots transport profile get");
- assert!(
- !serde_json::to_string(actions)
- .expect("actions json")
- .contains("nostr")
- );
-}
-
-#[test]
-fn transport_profile_local_only_output_uses_profile_and_local_status_fields() {
- let sandbox = RadrootsCliSandbox::new();
-
- let value = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]);
- let result = &value["result"];
- let statuses = result["transport_statuses"]
- .as_array()
- .expect("transport statuses");
- let local = &statuses[0];
-
- assert_eq!(value["operation_id"], "transport.profile.get");
- assert_eq!(result["profile_id"], "local_only");
- assert_eq!(result["profile_kind"], "local_only");
- assert_eq!(result["profile_delivery_usable"], false);
- assert!(result.get("transport_kind").is_none());
- assert!(result.get("implementation_state").is_none());
- assert_eq!(statuses.len(), 1);
- assert_eq!(local["transport"], "local");
- assert_eq!(local["profile_id"], "local_only");
- assert_eq!(local["implementation"], "real");
- assert_eq!(local["configured"], true);
- assert_eq!(local["usable_for_delivery"], false);
- assert_transport_capabilities(local, false, false);
- assert_eq!(
- local["message"],
- "Local-only profile writes only to local state"
- );
- assert_no_removed_transport_status_fields(local);
-}
-
-#[test]
-fn transport_profile_set_multi_target_persists_nostr_and_reticulum_config() {
- let sandbox = RadrootsCliSandbox::new();
-
- let value = sandbox.json_success(&[
+fn reads_reject_idempotency_keys() {
+ let value = json_failure(&[
"--format",
"json",
- "transport",
+ "--idempotency-key",
+ UUID_V7,
"profile",
- "set",
- "--kind",
- "multi-target",
- "--nostr-relay",
- "wss://relay.example.com",
- "--reticulum-behavior",
- "defer-delivery-plans",
- "--reticulum-scope",
- "farmers_market",
- "--reticulum-agent-endpoint",
- "reticulum-agent:local",
+ "inspect",
]);
- assert_eq!(value["operation_id"], "transport.profile.set");
- assert_eq!(value["result"]["profile_id"], "multi_target");
- assert_eq!(value["result"]["profile_kind"], "multi_target");
- assert!(value["result"].get("transport_kind").is_none());
- assert_eq!(value["result"]["state"], "configured");
- assert_eq!(
- value["result"]["nostr_relays"][0],
- "wss://relay.example.com"
- );
- assert_eq!(
- value["result"]["transport_statuses"]
- .as_array()
- .unwrap()
- .len(),
- 2
- );
- assert_eq!(
- value["result"]["transport_statuses"][0]["transport"],
- "nostr"
- );
- assert_eq!(
- value["result"]["transport_statuses"][0]["implementation"],
- "real"
- );
- assert_eq!(value["result"]["transport_statuses"][0]["configured"], true);
- assert_eq!(
- value["result"]["transport_statuses"][0]["usable_for_delivery"],
- true
- );
- assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false);
- assert_eq!(
- value["result"]["transport_statuses"][1]["transport"],
- "reticulum"
- );
- assert_eq!(
- value["result"]["transport_statuses"][1]["implementation"],
- "real"
- );
- assert_eq!(
- value["result"]["transport_statuses"][1]["maturity"],
- "preview"
- );
- assert_eq!(
- value["result"]["transport_statuses"][1]["availability"],
- "unavailable"
- );
- assert_eq!(value["result"]["transport_statuses"][1]["configured"], true);
- assert_eq!(
- value["result"]["transport_statuses"][1]["usable_for_delivery"],
- false
- );
- assert_transport_capabilities(&value["result"]["transport_statuses"][1], false, false);
- assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]);
- assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][1]);
- assert_eq!(
- value["result"]["reticulum_behavior"],
- "defer_delivery_plans"
- );
- assert_eq!(value["result"]["reticulum_scope"], "farmers_market");
- assert_eq!(
- value["result"]["reticulum_agent_endpoint"],
- "reticulum-agent:local"
- );
-
- let value = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]);
- assert_eq!(value["result"]["profile_id"], "multi_target");
- assert_eq!(
- value["result"]["nostr_relays"][0],
- "wss://relay.example.com"
- );
- assert_eq!(
- value["result"]["reticulum_behavior"],
- "defer_delivery_plans"
- );
- assert_eq!(value["result"]["reticulum_scope"], "farmers_market");
- assert_eq!(
- value["result"]["reticulum_agent_endpoint"],
- "reticulum-agent:local"
- );
-
- let config = fs::read_to_string(sandbox.root().join("config/apps/cli/config.toml"))
- .expect("read app config");
- assert!(config.contains("profile = \"multi_target\""));
- assert!(config.contains("[transport.nostr]"));
- assert!(config.contains("[transport.reticulum]"));
- assert!(config.contains("behavior = \"defer_delivery_plans\""));
- assert!(config.contains("scope = \"farmers_market\""));
- assert!(config.contains("agent_endpoint = \"reticulum-agent:local\""));
-}
-
-#[test]
-fn transport_status_multi_target_output_reports_nostr_and_reticulum_rows() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config(
- "[transport]\nprofile = \"multi_target\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.example.com\"]\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n",
- );
-
- let value = sandbox.json_success(&["--format", "json", "transport", "status"]);
- let result = &value["result"];
- let transports = result["transports"].as_array().expect("transports");
- let nostr = &transports[0];
- let reticulum = &transports[1];
-
- assert_eq!(value["operation_id"], "transport.status");
- assert_eq!(result["active_profile"]["profile_id"], "multi_target");
- assert_eq!(result["active_profile"]["profile_kind"], "multi_target");
- assert_eq!(result["active_profile"]["profile_delivery_usable"], true);
- assert_eq!(transports.len(), 2);
- assert_eq!(nostr["transport"], "nostr");
- assert_eq!(nostr["implementation"], "real");
- assert_eq!(nostr["configured"], true);
- assert_eq!(nostr["usable_for_delivery"], true);
- assert_transport_capabilities(nostr, true, false);
- assert_eq!(reticulum["transport"], "reticulum");
- assert_eq!(reticulum["implementation"], "real");
- assert_eq!(reticulum["maturity"], "preview");
- assert_eq!(reticulum["availability"], "unavailable");
- assert_eq!(reticulum["configured"], true);
- assert_eq!(reticulum["usable_for_delivery"], false);
- assert_transport_capabilities(reticulum, false, false);
- assert_eq!(reticulum["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE);
- assert_no_removed_transport_status_fields(nostr);
- assert_no_removed_transport_status_fields(reticulum);
-}
-
-#[test]
-fn transport_status_nostr_output_uses_canonical_transport_row() {
- let sandbox = RadrootsCliSandbox::new();
- configure_nostr_transport(&sandbox, "wss://relay.example.com");
-
- let value = sandbox.json_success(&["--format", "json", "transport", "status"]);
- let result = &value["result"];
- let transports = result["transports"].as_array().expect("transports");
- let nostr = &transports[0];
-
- assert_eq!(value["operation_id"], "transport.status");
- assert_eq!(result["active_profile"]["profile_id"], "nostr");
- assert_eq!(result["active_profile"]["profile_kind"], "nostr");
- assert_eq!(result["active_profile"]["profile_delivery_usable"], true);
- assert_eq!(transports.len(), 1);
- assert_eq!(nostr["transport"], "nostr");
- assert_eq!(nostr["profile_id"], "nostr");
- assert_eq!(nostr["implementation"], "real");
- assert_eq!(nostr["configured"], true);
- assert_eq!(nostr["usable_for_delivery"], true);
- assert_transport_capabilities(nostr, true, false);
- assert_no_removed_transport_status_fields(nostr);
-}
-
-#[test]
-fn transport_status_reticulum_output_reports_unusable_unavailable_state() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config(
- "[transport]\nprofile = \"reticulum\"\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n",
- );
-
- let value = sandbox.json_success(&["--format", "json", "transport", "status"]);
- let result = &value["result"];
- let transports = result["transports"].as_array().expect("transports");
- let active = &transports[0];
-
- assert_eq!(value["operation_id"], "transport.status");
- assert_eq!(result["state"], "ready");
- assert_eq!(transports.len(), 1);
- assert_eq!(result["active_profile"]["profile_id"], "reticulum");
- assert_eq!(result["active_profile"]["profile_kind"], "reticulum");
- assert_eq!(result["active_profile"]["profile_delivery_usable"], false);
- assert_eq!(active["transport"], "reticulum");
- assert_eq!(active["implementation"], "real");
- assert_eq!(active["maturity"], "preview");
- assert_eq!(active["availability"], "unavailable");
- assert_eq!(active["configured"], true);
- assert_eq!(active["usable_for_delivery"], false);
- assert_transport_capabilities(active, false, false);
- assert_eq!(active["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE);
+ assert_eq!(value["operation_id"], "profile.inspect");
+ assert_eq!(value["status"], "error");
+ assert_eq!(value["reason_code"], "invalid_input");
assert!(
- !active["message"]
+ value["errors"][0]["message"]
.as_str()
- .expect("message")
- .contains("Nostr")
- );
- assert_no_removed_transport_status_fields(active);
- assert!(
- !serde_json::to_string(&active["actions"])
- .expect("actions json")
- .contains("nostr")
- );
-}
-
-#[test]
-fn transport_outbox_status_human_output_reports_reticulum_deferred_and_publishing_counts() {
- let sandbox = RadrootsCliSandbox::new();
- seed_signed_transport_outbox_event(
- &sandbox,
- "deferred until implemented",
- "reticulum",
- transport_target(
- RadrootsTransportKind::Reticulum,
- RADROOTS_RETICULUM_ENDPOINT_URI,
- ),
- RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts,
- false,
- );
- seed_signed_transport_outbox_event(
- &sandbox,
- "deferred until implemented",
- "reticulum",
- transport_target(
- RadrootsTransportKind::Reticulum,
- RADROOTS_RETICULUM_ENDPOINT_URI,
- ),
- RadrootsOutboxReticulumBehavior::DeferDeliveryPlans,
- false,
- );
- seed_signed_transport_outbox_event(
- &sandbox,
- "publishing",
- "nostr",
- transport_target(RadrootsTransportKind::Nostr, ORDERABLE_LISTING_RELAY),
- RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts,
- true,
- );
-
- let output = sandbox
- .command()
- .args(["transport", "outbox", "status"])
- .output()
- .expect("run transport outbox status");
- assert!(
- output.status.success(),
- "stderr `{}` stdout `{}`",
- String::from_utf8_lossy(&output.stderr),
- String::from_utf8_lossy(&output.stdout)
+ .expect("error message")
+ .contains("forbids idempotency")
);
- let stdout = String::from_utf8(output.stdout).expect("terminal stdout");
-
- assert!(stdout.contains("Transport Outbox"), "{stdout}");
- assert_terminal_field(&stdout, "Deferred", "2");
- assert_terminal_field(&stdout, "Publishing", "1");
- assert_terminal_field(&stdout, "Ready signed", "0");
}
#[test]
-fn transport_outbox_status_read_only_succeeds_for_empty_nostr_profile() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config(
- "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = []\n",
- );
-
- let output = sandbox
- .command()
- .args(["transport", "outbox", "status"])
- .output()
- .expect("run transport outbox status");
+fn mutations_require_uuid_v7_idempotency_keys() {
+ let missing = json_failure(&["--format", "json", "--yes", "profile", "reset"]);
+ assert_eq!(missing["operation_id"], "profile.reset");
assert!(
- output.status.success(),
- "stderr `{}` stdout `{}`",
- String::from_utf8_lossy(&output.stderr),
- String::from_utf8_lossy(&output.stdout)
- );
- let stdout = String::from_utf8(output.stdout).expect("terminal stdout");
-
- assert_terminal_field(&stdout, "State", "unconfigured");
- assert_terminal_field(&stdout, "Profile", "nostr");
- assert_terminal_field(&stdout, "Total", "0");
- assert_terminal_field(&stdout, "Deferred", "0");
- assert_terminal_field(&stdout, "Publishing", "0");
-}
-
-#[test]
-fn config_get_reticulum_transport_exec_readiness_uses_shared_unavailable_copy() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config(
- "[transport]\nprofile = \"reticulum\"\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n",
- );
-
- let value = sandbox.json_success(&["--format", "json", "config", "get"]);
- let result = &value["result"];
-
- assert_eq!(value["operation_id"], "config.get");
- assert_eq!(result["publish"]["transport"], "reticulum");
- assert_eq!(result["publish"]["state"], "unavailable");
- assert_eq!(result["publish"]["executable"], false);
- assert_eq!(
- result["publish"]["reason"],
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
- );
- assert_eq!(
- result["publish"]["provider"]["reason"],
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
- );
- assert_eq!(
- result["write_plane"]["detail"],
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
- );
- assert_eq!(
- result["transport"]["message"],
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
+ missing["errors"][0]["message"]
+ .as_str()
+ .expect("missing idempotency message")
+ .contains("requires UUIDv7 idempotency")
);
- let result_json = serde_json::to_string(result).expect("result json");
- assert!(!result_json.contains("MVP network delivery"));
- assert!(!result_json.contains("non-networked in the MVP"));
-}
-#[test]
-fn mesh_status_reports_reticulum_unusable_state_without_fallback() {
- let sandbox = RadrootsCliSandbox::new();
-
- let value = sandbox.json_success(&["--format", "json", "mesh", "status"]);
- let result = &value["result"];
-
- assert_eq!(value["operation_id"], "mesh.status");
- assert_eq!(result["transport"], "reticulum");
- assert_eq!(result["configured"], false);
- assert_eq!(result["implementation"], "unavailable");
- assert_eq!(result["usable_for_delivery"], false);
- assert_no_removed_transport_status_fields(result);
- assert_eq!(
- result["message"],
- "Reticulum mesh delivery is unavailable until implementation"
- );
+ let invalid = json_failure(&[
+ "--format",
+ "json",
+ "--yes",
+ "--idempotency-key",
+ "profile-reset",
+ "profile",
+ "reset",
+ ]);
+ assert_eq!(invalid["operation_id"], "profile.reset");
assert!(
- !result["message"]
+ invalid["errors"][0]["message"]
.as_str()
- .expect("message")
- .contains("Nostr")
+ .expect("invalid idempotency message")
+ .contains("invalid UUIDv7 idempotency")
);
}
#[test]
-fn mesh_policy_check_reports_structured_delivery_denial() {
- let sandbox = RadrootsCliSandbox::new();
-
- let value = sandbox.json_success(&["--format", "json", "mesh", "policy", "check"]);
- let result = &value["result"];
+fn approval_required_operations_accept_explicit_yes_confirmation() {
+ let value = json_output(&[
+ "--format",
+ "json",
+ "--dry-run",
+ "--yes",
+ "--idempotency-key",
+ UUID_V7,
+ "profile",
+ "reset",
+ ]);
- assert_eq!(value["operation_id"], "mesh.policy.check");
- assert_eq!(result["scope"], "disabled");
- assert_eq!(result["policy"], "reticulum_mesh_delivery");
- assert_eq!(result["transport"], "reticulum");
- assert_eq!(result["usable_for_delivery"], false);
- assert_eq!(result["decision"], "denied");
- assert_eq!(result["deny_reason"], "delivery_unavailable");
- assert_eq!(result["privacy_class"], "public_event");
- assert_eq!(result["payload_bytes"], 1);
- assert_eq!(result["frame_bytes"], 1);
- assert_eq!(result["max_payload_bytes"], 0);
- assert_eq!(result["max_frame_bytes"], 0);
- assert_eq!(result["compression"], "disabled");
- assert_eq!(result["custom_scopes_enabled"], false);
- assert_eq!(
- result["message"],
- "Reticulum mesh delivery is unavailable until implementation"
- );
- assert!(
- !result["message"]
- .as_str()
- .expect("message")
- .contains("Nostr")
- );
+ assert_eq!(value["operation_id"], "profile.reset");
+ assert_eq!(value["status"], "ok");
+ assert_eq!(value["dry_run"], true);
+ assert_eq!(value["idempotency_key"], UUID_V7);
}
#[test]
-fn transport_source_boundary_rejects_removed_relay_and_daemon_transport_surfaces() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let removed_daemon_transport_alias = ["radrootsd_", "pro", "xy"].concat();
- let removed_daemon_publish_alias = ["radrootsd.publish_", "pro", "xy.v1"].concat();
-
- assert!(
- !manifest_dir.join("src/cli/relay.rs").exists(),
- "removed relay command module must not be reintroduced"
- );
-
- for relative_path in [
- "src/runtime/transport.rs",
- "src/runtime/sdk.rs",
- "src/runtime/sync.rs",
- "src/cli/transport.rs",
- ] {
- let source = fs::read_to_string(manifest_dir.join(relative_path)).expect("read source");
- for forbidden in [
- removed_daemon_transport_alias.as_str(),
- removed_daemon_publish_alias.as_str(),
- "publish.relays.resolve",
- "\"publish.event\"",
- "transport_kinds",
- "allowed_relay_policy",
- "relay_policy",
- ] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not contain removed transport surface `{forbidden}`"
- );
- }
- }
-
- for relative_path in ["src/ops/error.rs", "src/view/runtime.rs"] {
- let source = fs::read_to_string(manifest_dir.join(relative_path)).expect("read source");
- for forbidden in ["bridge_enabled", "bridge_ready", "bridge auth"] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not contain stale transport bridge term `{forbidden}`"
- );
- }
- }
-
- let registry_source =
- fs::read_to_string(manifest_dir.join("src/registry/mod.rs")).expect("read registry source");
- let main_source = fs::read_to_string(manifest_dir.join("src/main.rs")).expect("read main");
- let farm_exec_source =
- fs::read_to_string(manifest_dir.join("src/ops/exec/farm.rs")).expect("read farm ops");
- let listing_exec_source =
- fs::read_to_string(manifest_dir.join("src/ops/exec/listing.rs")).expect("read listing ops");
- let farm_runtime_source =
- fs::read_to_string(manifest_dir.join("src/runtime/farm.rs")).expect("read farm runtime");
- for (relative_path, source) in [
- ("src/registry/mod.rs", registry_source.as_str()),
- ("src/main.rs", main_source.as_str()),
- ("src/ops/exec/farm.rs", farm_exec_source.as_str()),
- ("src/ops/exec/listing.rs", listing_exec_source.as_str()),
- ("src/runtime/farm.rs", farm_runtime_source.as_str()),
- ] {
- for forbidden in [
- "requires_nostr_transport_profile",
- "registry_nostr_publish_requirements_are_explicit",
- "is_transport_profile_routed_operation",
- "farm_publish_relay_unavailable",
- "require_relay_target",
- "listing_relay_unavailable",
- "relay_farm_publish_readiness",
- ] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not retain stale transport-profile helper name `{forbidden}`"
- );
- }
- }
- for (relative_path, source, required) in [
- (
- "src/registry/mod.rs",
- registry_source.as_str(),
- "requires_delivery_capable_transport_profile",
- ),
- (
- "src/main.rs",
- main_source.as_str(),
- "requires_delivery_capable_transport_profile",
- ),
- (
- "src/ops/exec/farm.rs",
- farm_exec_source.as_str(),
- "farm_publish_transport_delivery_unavailable",
- ),
- (
- "src/ops/exec/farm.rs",
- farm_exec_source.as_str(),
- "require_nostr_delivery_target",
- ),
- (
- "src/ops/exec/listing.rs",
- listing_exec_source.as_str(),
- "listing_transport_delivery_unavailable",
- ),
- (
- "src/runtime/farm.rs",
- farm_runtime_source.as_str(),
- "transport_farm_publish_readiness",
- ),
- ] {
- assert!(
- source.contains(required),
- "{relative_path} must retain delivery-capable transport-profile witness `{required}`"
- );
- }
-
- let transport_source =
- fs::read_to_string(manifest_dir.join("src/runtime/transport.rs")).expect("read source");
- let view_source =
- fs::read_to_string(manifest_dir.join("src/view/runtime.rs")).expect("read runtime view");
- for required in [
- "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE",
- "RadrootsTransportStatus::new",
- "RadrootsTransportKind::Reticulum",
- "transport_runtime_status_view",
- "RadrootsTransportCapabilityMaturity::Preview",
- "RadrootsTransportCapabilityAvailability::Unavailable",
- ] {
- assert!(
- transport_source.contains(required),
- "src/runtime/transport.rs must retain transport status/message witness `{required}`"
- );
- }
+fn binary_v1_catalog_has_no_retired_operation_ids() {
+ let operation_ids = RUNTIME_OPERATION_DESCRIPTORS_V1
+ .iter()
+ .map(|descriptor| descriptor.operation_id.as_str())
+ .collect::<Vec<_>>();
- let transport_status_view_source = source_window(
- view_source.as_str(),
- "pub struct TransportRuntimeStatusView",
- "pub struct TransportOutboxStatusView",
- );
- for required in [
- "pub transport: String,",
- "pub configured: bool,",
- "pub implementation: String,",
- "pub maturity: String,",
- "pub availability: String,",
- "pub usable_for_delivery: bool,",
- "pub capabilities: TransportOperationCapabilitiesView,",
- "pub struct TransportOperationCapabilitiesView",
- "pub deliver: bool,",
- "pub fetch: bool,",
- "pub message: String,",
- ] {
- assert!(
- transport_status_view_source.contains(required),
- "transport status view must retain canonical field `{required}`"
- );
- }
- for forbidden in [
- "pub transport_kind:",
- "pub implementation_state:",
- "pub readiness:",
- "pub publish_usable:",
- "pub fetch_usable:",
- "pub redacted_message:",
- ] {
- assert!(
- !transport_status_view_source.contains(forbidden),
- "transport status view must not expose removed field `{forbidden}`"
- );
+ for retired in [
+ vec!["workspace", "get"],
+ vec!["config", "get"],
+ vec!["mesh", "status"],
+ vec!["trade", "submit"],
+ vec!["listing", "archive"],
+ vec!["validation", "receipt", "list"],
+ ]
+ .into_iter()
+ .map(|parts| parts.join("."))
+ {
+ assert!(!operation_ids.contains(&retired.as_str()), "{retired}");
}
-
- let mesh_scope_view_source = source_window(
- view_source.as_str(),
- "pub struct MeshScopeView",
- "pub struct MeshStatusView",
- );
- assert!(
- mesh_scope_view_source.contains("pub implementation: String,"),
- "mesh scope view must retain canonical implementation field"
- );
- assert!(
- !mesh_scope_view_source.contains("pub implementation_state:"),
- "mesh scope view must not expose removed implementation_state field"
- );
-
- let mesh_status_view_source = source_window(
- view_source.as_str(),
- "pub struct MeshStatusView",
- "pub struct MeshPolicyCheckView",
- );
- for required in [
- "pub transport: String,",
- "pub configured: bool,",
- "pub implementation: String,",
- "pub usable_for_delivery: bool,",
- "pub message: String,",
- ] {
- assert!(
- mesh_status_view_source.contains(required),
- "mesh status view must retain canonical field `{required}`"
- );
- }
- for forbidden in [
- "pub transport_kind:",
- "pub configured_state:",
- "pub implementation_state:",
- ] {
- assert!(
- !mesh_status_view_source.contains(forbidden),
- "mesh status view must not expose removed field `{forbidden}`"
- );
- }
-
- let mesh_policy_view_source = source_window(
- view_source.as_str(),
- "pub struct MeshPolicyCheckView",
- "pub struct RpcStatusView",
- );
- for required in [
- "pub transport: String,",
- "pub usable_for_delivery: bool,",
- "pub decision: String,",
- "pub deny_reason: String,",
- "pub payload_bytes: u64,",
- "pub frame_bytes: u64,",
- "pub max_payload_bytes: u64,",
- "pub max_frame_bytes: u64,",
- "pub compression: String,",
- "pub custom_scopes_enabled: bool,",
- ] {
- assert!(
- mesh_policy_view_source.contains(required),
- "mesh policy view must retain structured denial field `{required}`"
- );
- }
- assert!(
- !mesh_policy_view_source.contains("pub transport_kind:"),
- "mesh policy view must not expose removed transport_kind field"
- );
- for forbidden in [
- "pub accepted:",
- "pub delivered:",
- "pub forwarded:",
- "pub stored:",
- "pub seen:",
- ] {
- assert!(
- !mesh_policy_view_source.contains(forbidden),
- "mesh policy view must not expose unavailable-delivery success field `{forbidden}`"
- );
- }
-
- let mesh_runtime_source =
- fs::read_to_string(manifest_dir.join("src/runtime/mesh.rs")).expect("read mesh runtime");
- for required in [
- "RadrootsMeshPayloadPolicy::reticulum_unavailable()",
- "policy.evaluate(&input)",
- "decision.usable_for_delivery()",
- "deny_reason",
- "policy.max_payload_bytes",
- "policy.max_frame_bytes",
- "policy.compression.label()",
- "policy.custom_scopes_enabled",
- ] {
- assert!(
- mesh_runtime_source.contains(required),
- "src/runtime/mesh.rs must derive structured delivery denial from mesh policy witness `{required}`"
- );
- }
- for forbidden in [
- "fallback to Nostr",
- "falls back to Nostr",
- "accepted delivery",
- "delivered delivery",
- "forwarded delivery",
- "stored delivery",
- "seen delivery",
- ] {
- assert!(
- !mesh_runtime_source.contains(forbidden),
- "src/runtime/mesh.rs must not imply Reticulum delivery success or fallback `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn foundation_hardening_sources_reject_retired_names_and_ambiguous_docs() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let mut findings = Vec::new();
-
- for path in cli_foundation_hardening_guard_files(manifest_dir) {
- let source = fs::read_to_string(path.as_path()).expect("read source");
- let relative_path = path
- .strip_prefix(manifest_dir)
- .expect("guard path under manifest")
- .to_string_lossy()
- .replace('\\', "/");
-
- for (pattern, reason) in FORBIDDEN_FOUNDATION_HARDENING_RETIRED_CONCEPTS {
- if foundation_hardening_retired_concept_allowed(relative_path.as_str(), pattern) {
- continue;
- }
- if contains_foundation_hardening_concept(source.as_str(), pattern) {
- findings.push(format!(
- "{relative_path} contains retired Foundation Hardening concept `{pattern}`: {reason}"
- ));
- }
- }
-
- if is_foundation_doc_surface(path.as_path()) {
- for (pattern, reason) in FORBIDDEN_FOUNDATION_HARDENING_DOC_CONCEPTS {
- if source.contains(pattern) {
- findings.push(format!(
- "{relative_path} contains ambiguous Foundation Hardening wording `{pattern}`: {reason}"
- ));
- }
- }
- }
- }
-
- assert!(
- findings.is_empty(),
- "CLI Foundation Hardening V1 source-boundary violations:\n{}",
- findings.join("\n")
- );
-}
-
-#[test]
-fn sync_transport_status_source_boundary_rejects_retired_relay_shaped_generic_output() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let view_source =
- fs::read_to_string(manifest_dir.join("src/view/runtime.rs")).expect("read runtime view");
- let runtime_source =
- fs::read_to_string(manifest_dir.join("src/runtime/sync.rs")).expect("read sync runtime");
-
- for required in [
- "pub configured_transport_target_count: usize,",
- "pub configured_transport_targets: Vec<SyncTransportTargetView>,",
- "pub transport_statuses: Vec<SyncTransportStatusView>,",
- "pub transport: String,",
- "pub configured: bool,",
- "pub implementation: String,",
- "pub usable_for_delivery: bool,",
- "pub capabilities: TransportOperationCapabilitiesView,",
- "pub message: String,",
- "pub target_transport_endpoints: Vec<String>,",
- "pub attempted_transport_endpoints: Vec<String>,",
- "pub accepted_transport_endpoints: Vec<String>,",
- "pub failed_transport_targets: Vec<TransportTargetFailureView>,",
- ] {
- assert!(
- view_source.contains(required),
- "sync view source must retain transport-neutral field witness `{required}`"
- );
- }
-
- let sync_status_view_source = source_window(
- view_source.as_str(),
- "pub struct SyncTransportStatusView",
- "pub struct TransportTargetFailureView",
- );
- for required in [
- "pub capabilities: TransportOperationCapabilitiesView,",
- "pub message: String,",
- ] {
- assert!(
- sync_status_view_source.contains(required),
- "sync transport status view must retain canonical field `{required}`"
- );
- }
- for forbidden in [
- "pub transport_kind:",
- "pub implementation_state:",
- "pub readiness:",
- "pub publish_usable:",
- "pub fetch_usable:",
- "pub redacted_message:",
- ] {
- assert!(
- !sync_status_view_source.contains(forbidden),
- "sync transport status view must not expose removed field `{forbidden}`"
- );
- }
-
- for required in [
- "fn sdk_sync_status_view",
- "fn sync_transport_metadata",
- "fn sync_configured_transport_targets",
- "fn sdk_push_action_view",
- "fn sdk_transport_targets",
- "fn sdk_push_target_transport_endpoints",
- "fn sdk_push_failed_transport_targets",
- ] {
- assert!(
- runtime_source.contains(required),
- "sync runtime source must retain transport-neutral mapping witness `{required}`"
- );
- }
-
- for guarded_source in [
- source_window(
- view_source.as_str(),
- "pub struct SyncStatusView",
- "impl SyncStatusView",
- ),
- source_window(
- view_source.as_str(),
- "pub struct SyncActionView",
- "impl SyncActionView",
- ),
- source_window(
- runtime_source.as_str(),
- "fn sdk_sync_status_view",
- "fn sdk_push_dry_run_view",
- ),
- source_window(
- runtime_source.as_str(),
- "fn sdk_push_action_view",
- "fn sdk_transport_targets",
- ),
- ] {
- for forbidden in [
- concat!("configured_nostr", "_relay", "_count"),
- concat!("configured_nostr", "_relays"),
- concat!("target", "_relays"),
- concat!("connected", "_relays"),
- concat!("acknowledged", "_relays"),
- concat!("failed", "_relays"),
- concat!("relay", "_count"),
- ] {
- assert!(
- !guarded_source.contains(forbidden),
- "generic sync source must not expose retired relay-shaped field `{forbidden}`"
- );
- }
- }
-}
-
-fn foundation_hardening_retired_concept_allowed(relative_path: &str, pattern: &str) -> bool {
- pattern == "RadrootsNostrEvent" && cli_nostr_protocol_context(relative_path)
-}
-
-fn cli_nostr_protocol_context(relative_path: &str) -> bool {
- matches!(
- relative_path,
- "src/runtime/order.rs" | "src/runtime/sdk.rs" | "src/runtime/sync.rs"
- )
-}
-
-fn cli_foundation_hardening_guard_files(manifest_dir: &Path) -> Vec<PathBuf> {
- let mut paths = Vec::new();
-
- for path in [
- manifest_dir.join("Cargo.toml"),
- manifest_dir.join("README"),
- manifest_dir.join("README.md"),
- ] {
- if path.exists() {
- paths.push(path);
- }
- }
-
- for relative_root in ["src", "docs"] {
- let root = manifest_dir.join(relative_root);
- if root.exists() {
- collect_foundation_hardening_guard_files(root.as_path(), &mut paths);
- }
- }
-
- paths.sort();
- paths
-}
-
-fn collect_foundation_hardening_guard_files(root: &Path, paths: &mut Vec<PathBuf>) {
- for entry in fs::read_dir(root)
- .unwrap_or_else(|error| panic!("failed to read {}: {error}", root.display()))
- {
- let path = entry.expect("guard entry").path();
- if path.is_dir() {
- collect_foundation_hardening_guard_files(path.as_path(), paths);
- continue;
- }
-
- let guarded_extension = matches!(
- path.extension().and_then(|extension| extension.to_str()),
- Some("rs") | Some("toml") | Some("md")
- );
- if guarded_extension
- || matches!(
- path.file_name().and_then(|file_name| file_name.to_str()),
- Some("README") | Some("README.md")
- )
- {
- paths.push(path);
- }
- }
-}
-
-fn contains_foundation_hardening_concept(source: &str, pattern: &str) -> bool {
- if !pattern.chars().all(is_foundation_identifier_character) {
- return source.contains(pattern);
- }
-
- source.match_indices(pattern).any(|(index, _)| {
- let before = source[..index].chars().next_back();
- let after = source[index + pattern.len()..].chars().next();
- before.is_none_or(|character| !is_foundation_identifier_character(character))
- && after.is_none_or(|character| !is_foundation_identifier_character(character))
- })
-}
-
-fn is_foundation_identifier_character(character: char) -> bool {
- character == '_' || character.is_ascii_alphanumeric()
-}
-
-fn is_foundation_doc_surface(path: &Path) -> bool {
- matches!(
- path.file_name().and_then(|file_name| file_name.to_str()),
- Some("README") | Some("README.md")
- ) || matches!(
- path.extension().and_then(|extension| extension.to_str()),
- Some("md")
- )
-}
-
-fn source_window<'source>(
- source: &'source str,
- start_marker: &str,
- end_marker: &str,
-) -> &'source str {
- let start = source
- .find(start_marker)
- .unwrap_or_else(|| panic!("failed to find source marker `{start_marker}`"));
- let after_start = &source[start..];
- let end = after_start
- .find(end_marker)
- .unwrap_or_else(|| panic!("failed to find source marker `{end_marker}`"));
- &after_start[..end]
-}
-
-fn help_lists(stdout: &str, command: &str) -> bool {
- stdout.lines().any(|line| {
- let line = line.trim_start();
- line == command || line.starts_with(&format!("{command} "))
- })
-}
-
-#[test]
-fn removed_global_flags_are_rejected_publicly() {
- for args in [
- ["--output", "json", "workspace", "get"].as_slice(),
- ["--json", "workspace", "get"].as_slice(),
- ["--ndjson", "workspace", "get"].as_slice(),
- ["--yes", "workspace", "get"].as_slice(),
- ["--non-interactive", "workspace", "get"].as_slice(),
- ["--signer", "myc", "workspace", "get"].as_slice(),
- ["--farm-id", "farm_test", "workspace", "get"].as_slice(),
- ["--profile", "repo_local", "workspace", "get"].as_slice(),
- ["--signer-session-id", "session_test", "workspace", "get"].as_slice(),
- ] {
- let output = radroots().args(args).output().expect("run removed flag");
-
- assert!(!output.status.success(), "`{args:?}` should be rejected");
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("unexpected argument") || stderr.contains("unrecognized"));
- }
-}
-
-#[test]
-fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
- let sandbox = RadrootsCliSandbox::new();
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:19001");
-
- let value = sandbox.json_success(&["--format", "json", "config", "get"]);
-
- assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["transport"], "nostr");
- assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
- assert_eq!(value["result"]["publish"]["signed_write_required"], true);
- assert_eq!(value["result"]["publish"]["state"], "unconfigured");
- assert_eq!(value["result"]["publish"]["executable"], false);
- assert_contains(
- &value["result"]["publish"]["reason"],
- "write-capable local account",
- );
- assert_eq!(
- value["result"]["publish"]["provider"]["state"],
- "unconfigured"
- );
- assert_eq!(
- value["result"]["write_plane"]["provider_runtime_id"],
- "nostr"
- );
- assert_eq!(
- value["result"]["write_plane"]["binding_model"],
- "nostr_transport"
- );
- assert_eq!(value["result"]["write_plane"]["state"], "unconfigured");
- assert_eq!(value["result"]["rpc"], Value::Null);
- assert_contains(
- &value["result"]["write_plane"]["detail"],
- "write-capable local account",
- );
- assert_eq!(value["result"]["actions"][0], "radroots account create");
- assert_eq!(
- value["next_actions"][0]["command"],
- "radroots account create"
- );
- assert_no_daemon_runtime_reference(&value, &["--format", "json", "config", "get"]);
-}
-
-#[test]
-fn config_get_marks_relay_publish_ready_with_secret_backed_local_account() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:19002");
-
- let value = sandbox.json_success(&["--format", "json", "config", "get"]);
-
- assert_eq!(value["result"]["publish"]["transport"], "nostr");
- assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
- assert_eq!(value["result"]["publish"]["signed_write_required"], true);
- assert_eq!(value["result"]["publish"]["state"], "ready");
- assert_eq!(value["result"]["publish"]["executable"], true);
- assert_eq!(value["result"]["publish"]["reason"], Value::Null);
- assert_eq!(value["result"]["publish"]["provider"]["state"], "ready");
-}
-
-#[test]
-fn config_get_marks_relay_publish_unconfigured_with_missing_myc_binding() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.write_app_config("[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"ws://127.0.0.1:19003\"]\n\n[signer]\nbackend = \"myc\"\n");
-
- let value = sandbox.json_success(&["--format", "json", "config", "get"]);
-
- assert_eq!(value["result"]["publish"]["transport"], "nostr");
- assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
- assert_eq!(value["result"]["publish"]["signed_write_required"], true);
- assert_eq!(value["result"]["publish"]["state"], "unconfigured");
- assert_eq!(value["result"]["publish"]["executable"], false);
- assert_contains(
- &value["result"]["publish"]["reason"],
- "signer.remote_nip46 binding is missing",
- );
- assert_eq!(
- value["result"]["publish"]["provider"]["state"],
- "unconfigured"
- );
-}
-
-#[test]
-fn config_get_marks_relay_publish_unconfigured_with_watch_only_account() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(41);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "publish-readiness-watch-only", &public_identity);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
-
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:19004");
- let value = sandbox.json_success(&["--format", "json", "config", "get"]);
-
- assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
- assert_eq!(value["result"]["publish"]["signed_write_required"], true);
- assert_eq!(value["result"]["publish"]["state"], "unconfigured");
- assert_eq!(value["result"]["publish"]["executable"], false);
- assert_contains(&value["result"]["publish"]["reason"], "watch_only");
-}
-
-#[test]
-fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() {
- let sandbox = RadrootsCliSandbox::new();
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:19005");
-
- let value = sandbox.json_success(&["--format", "json", "health", "status", "get"]);
-
- assert_eq!(value["operation_id"], "health.status.get");
- assert_eq!(value["result"]["state"], "needs_attention");
- assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
- assert_eq!(value["result"]["publish"]["signed_write_required"], true);
- assert_eq!(value["result"]["publish"]["state"], "unconfigured");
- assert_eq!(value["result"]["publish"]["executable"], false);
- assert_contains(
- &value["result"]["publish"]["reason"],
- "write-capable local account",
- );
- assert_eq!(value["result"]["store"]["state"], "ready");
- assert_eq!(value["result"]["store"]["canonical_store"], "sdk");
- assert_eq!(value["result"]["signer"]["state"], "unconfigured");
- assert_eq!(value["result"]["actions"][0], "radroots account create");
- assert_eq!(
- value["next_actions"][0]["command"],
- "radroots account create"
- );
-}
-
-#[test]
-fn health_check_marks_relay_publish_ready_with_secret_backed_local_account() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "workspace", "init"]);
- sandbox.json_success(&["--format", "json", "account", "create"]);
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:19006");
-
- let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]);
-
- assert_eq!(value["operation_id"], "health.check.run");
- assert_eq!(value["result"]["state"], "ready");
- assert_eq!(value["result"]["account_resolution"]["status"], "resolved");
- assert_eq!(
- value["result"]["account_resolution"]["source"],
- "default_account"
- );
- assert_eq!(
- value["result"]["account_resolution"]["resolved_account"]["custody"],
- "secret_backed"
- );
- assert_eq!(
- value["result"]["account_resolution"]["resolved_account"]["write_capable"],
- true
- );
- assert_eq!(value["result"]["checks"]["publish"]["transport"], "nostr");
- assert_eq!(value["result"]["checks"]["publish"]["state"], "ready");
- assert_eq!(value["result"]["checks"]["publish"]["executable"], true);
- assert_eq!(
- value["result"]["actions"]
- .as_array()
- .expect("actions")
- .len(),
- 0
- );
- assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
-}
-
-#[test]
-fn farm_readiness_check_reports_mode_specific_publish_gates() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Ready Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
-
- let (_, relay_value) = sandbox.json_output(&["--format", "json", "farm", "readiness", "check"]);
- let relay_detail = if relay_value["result"].is_null() {
- &relay_value["errors"][0]["detail"]
- } else {
- &relay_value["result"]
- };
- assert_eq!(relay_detail["transport_profile"], "local_only");
- assert_eq!(relay_detail["publish_state"], "ready");
- assert_eq!(relay_detail["publish_executable"], true);
-}
-
-#[test]
-fn direct_publish_listing_uses_myc_nip46_sdk_signer() {
- let sandbox = RadrootsCliSandbox::new();
- let user_identity = identity_secret(90);
- let client_identity = identity_secret(91);
- let remote_identity = identity_secret(92);
- let user_public = user_identity.to_public();
- let user_keys = nostr_keys_from_identity(&user_identity);
- let remote_keys = nostr_keys_from_identity(&remote_identity);
- let remote_pubkey = remote_keys.public_key().to_hex();
- let relay = Nip46RelayServer::new(remote_keys.clone(), user_keys);
- let relay_endpoint = relay.endpoint().to_owned();
- let public_identity_file =
- write_public_identity_profile(&sandbox, "myc-direct-user", &user_public);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- store_test_session_secret(
- &sandbox,
- "session_ready",
- client_identity.secret_key_hex().as_str(),
- );
- sandbox.write_app_config(
- myc_nip46_config(
- remote_pubkey.as_str(),
- relay_endpoint.as_str(),
- account_id,
- "session_ready",
- )
- .as_str(),
- );
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- assert_eq!(signer["result"]["state"], "ready");
- assert_eq!(signer["result"]["binding"]["state"], "ready");
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Myc Relay Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let listing_file = create_listing_draft(&sandbox, "myc-direct-relay-live");
- make_listing_publishable(
- &listing_file,
- farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag"),
- );
-
- sandbox.write_app_config(
- format!(
- r#"[transport]
-profile = "nostr"
-
-[transport.nostr]
-relay_urls = ["{}"]
-
-{}"#,
- toml_string(relay_endpoint.as_str()),
- myc_nip46_config(
- remote_pubkey.as_str(),
- relay_endpoint.as_str(),
- account_id,
- "session_ready",
- )
- )
- .as_str(),
- );
- let output = sandbox
- .command()
- .args([
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000100",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ])
- .output()
- .expect("run myc direct listing publish");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
-
- assert!(
- output.status.success(),
- "stderr `{}` stdout `{}`",
- String::from_utf8_lossy(&output.stderr),
- String::from_utf8_lossy(&output.stdout)
- );
- let report = relay.join();
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"]["state"], "published");
- assert_eq!(
- value["result"]["source"],
- "SDK listing publish · configured signer"
- );
- assert_eq!(
- value["result"]["target_transport_endpoints"][0],
- relay_endpoint
- );
- assert!(report.connection_count >= 1);
- assert!(report.req_count >= 1);
- assert_eq!(report.sign_request_count, 1);
- assert_eq!(report.published_events.len(), 1);
- let published = &report.published_events[0];
- assert_eq!(published.kind, Kind::Custom(KIND_LISTING as u16));
- assert_eq!(published.pubkey.to_hex(), user_public.public_key_hex);
- assert_eq!(
- published.id.to_hex(),
- value["result"]["event_id"].as_str().expect("event id")
- );
-}
-
-#[test]
-fn listing_update_publish_attempts_direct_publish_with_approval() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Update Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let listing_file = create_listing_draft(&sandbox, "update-unavailable");
- make_listing_publishable(
- &listing_file,
- farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag"),
- );
-
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000103",
- "listing",
- "update",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.update");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "network");
- assert_contains(
- &value["errors"][0]["message"],
- "SDK transport publish did not reach accepted quorum",
- );
- assert!(
- !value["errors"][0]["message"]
- .as_str()
- .expect("error message")
- .contains("not implemented")
- );
- assert_no_removed_command_reference(&value, &["listing", "update"]);
- assert_no_daemon_runtime_reference(&value, &["listing", "update"]);
-}
-
-#[test]
-fn removed_order_submit_watch_flag_is_rejected_publicly() {
- let output = radroots()
- .args(["trade", "submit", "--watch"])
- .output()
- .expect("run removed trade submit watch flag");
-
- assert!(!output.status.success());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("unexpected argument") || stderr.contains("unrecognized"));
-}
-
-#[test]
-fn removed_command_families_are_rejected_publicly() {
- for command in [
- "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "product",
- "runtime", "job", "message", "approval", "agent",
- ] {
- let output = radroots()
- .arg(command)
- .output()
- .expect("run removed command");
-
- assert!(!output.status.success(), "`{command}` should be rejected");
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("unrecognized subcommand"));
- }
-}
-
-#[test]
-fn seller_order_decision_and_status_commands_are_public() {
- for (operation_id, args) in [
- (
- "trade.accept",
- [
- "--format",
- "json",
- "--dry-run",
- "trade",
- "accept",
- "ord_public",
- ]
- .as_slice(),
- ),
- (
- "trade.decline",
- [
- "--format",
- "json",
- "--dry-run",
- "trade",
- "decline",
- "ord_public",
- "--reason",
- "out_of_stock",
- ]
- .as_slice(),
- ),
- (
- "trade.cancel",
- [
- "--format",
- "json",
- "--dry-run",
- "trade",
- "cancel",
- "ord_public",
- "--reason",
- "changed plans",
- ]
- .as_slice(),
- ),
- (
- "trade.status.get",
- ["--format", "json", "trade", "status", "get", "ord_public"].as_slice(),
- ),
- ] {
- let output = radroots()
- .args(args)
- .output()
- .expect("run seller order command");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(value["operation_id"], operation_id);
- assert_ne!(
- String::from_utf8(output.stderr).expect("utf8 stderr"),
- "unrecognized subcommand"
- );
- }
-
- let output = radroots()
- .args(["trade", "decision", "accept", "ord_deferred"])
- .output()
- .expect("run removed nested decision command");
-
- assert!(!output.status.success());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("unrecognized subcommand"));
-}
-
-#[test]
-fn removed_order_post_agreement_subcommands_are_rejected_publicly() {
- for args in [
- ["trade", "fulfillment", "update", "ord_public"].as_slice(),
- ["trade", "receipt", "record", "ord_public"].as_slice(),
- ["trade", "payment", "record", "ord_public"].as_slice(),
- ["trade", "settlement", "accept", "ord_public"].as_slice(),
- ["trade", "settlement", "reject", "ord_public"].as_slice(),
- ] {
- let output = radroots()
- .args(args)
- .output()
- .expect("run removed order command");
-
- assert!(!output.status.success(), "`{args:?}` should be rejected");
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("unrecognized subcommand"));
- }
-}
-
-#[test]
-fn target_outputs_do_not_suggest_removed_command_families() {
- let sandbox = RadrootsCliSandbox::new();
-
- for args in [
- ["--format", "json", "market", "product", "search", "eggs"].as_slice(),
- ["--format", "json", "market", "listing", "get", "eggs"].as_slice(),
- ["--format", "json", "listing", "get", "eggs"].as_slice(),
- ["--format", "json", "listing", "list"].as_slice(),
- [
- "--format",
- "json",
- "trade",
- "get",
- "ord_AAAAAAAAAAAAAAAAAAAAAA",
- ]
- .as_slice(),
- ] {
- let value = sandbox.json_success(args);
- assert_no_removed_command_reference(&value, args);
- }
-
- let sync_args = ["--format", "json", "sync", "status", "get"];
- let value = sandbox.json_success(&sync_args);
- assert_eq!(value["operation_id"], "sync.status.get");
- assert_eq!(
- value["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_no_removed_command_reference(&value, &sync_args);
-}
-
-#[test]
-fn listing_list_reports_empty_local_draft_state_truthfully() {
- let sandbox = RadrootsCliSandbox::new();
- let value = sandbox.json_success(&["--format", "json", "listing", "list"]);
-
- assert_eq!(value["operation_id"], "listing.list");
- assert_eq!(value["result"]["state"], "empty");
- assert_eq!(value["result"]["count"], 0);
- assert_eq!(
- value["result"]["listings"]
- .as_array()
- .expect("listings")
- .len(),
- 0
- );
- assert!(
- value["result"]["draft_dir"]
- .as_str()
- .expect("draft dir")
- .ends_with("listings/drafts")
- );
- assert_no_removed_command_reference(&value, &["listing", "list"]);
-}
-
-#[test]
-fn listing_list_reports_default_local_drafts() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let create = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "create",
- "--key",
- "eggs",
- "--title",
- "Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- let listing_file = create["result"]["file"].as_str().expect("listing file");
- assert!(Path::new(listing_file).exists());
-
- let value = sandbox.json_success(&["--format", "json", "listing", "list"]);
- let listing = &value["result"]["listings"][0];
-
- assert_eq!(value["operation_id"], "listing.list");
- assert_eq!(value["result"]["state"], "ready");
- assert_eq!(value["result"]["count"], 1);
- assert_eq!(listing["id"], create["result"]["listing_id"]);
- assert_eq!(listing["state"], "ready");
- assert_eq!(listing["file"], listing_file);
- assert_eq!(listing["product_key"], "eggs");
- assert_eq!(listing["title"], "Eggs");
- assert_eq!(listing["category"], "eggs");
- assert_eq!(listing["location_primary"], "farmstand");
- assert!(listing["seller_pubkey"].is_string());
- assert!(listing["farm_d_tag"].is_string());
- assert_no_removed_command_reference(&value, &["listing", "list"]);
-}
-
-#[test]
-fn listing_rebind_updates_seller_actor_with_approval() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- let listing_file = create_listing_draft(&sandbox, "rebind-listing");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- let initial_validation = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "validate",
- listing_file.to_string_lossy().as_ref(),
- ]);
- let first_pubkey = initial_validation["result"]["seller_pubkey"]
- .as_str()
- .expect("first pubkey");
- let before = fs::read_to_string(&listing_file).expect("listing before");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
-
- let dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "rebind",
- listing_file.to_string_lossy().as_ref(),
- second_account_id,
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
- ]);
- assert_eq!(dry_run["operation_id"], "listing.rebind");
- assert_eq!(dry_run["result"]["state"], "dry_run");
- assert_eq!(
- dry_run["result"]["from_seller_account_id"],
- first_account_id
- );
- assert_eq!(dry_run["result"]["from_seller_pubkey"], first_pubkey);
- assert_eq!(dry_run["result"]["to_seller_account_id"], second_account_id);
- let second_pubkey = dry_run["result"]["to_seller_pubkey"]
- .as_str()
- .expect("second pubkey");
- assert_eq!(dry_run["result"]["seller_pubkey_changed"], true);
- assert_eq!(
- fs::read_to_string(&listing_file).expect("listing after dry-run"),
- before
- );
-
- let unapproved = sandbox.json_output(&[
- "--format",
- "json",
- "listing",
- "rebind",
- listing_file.to_string_lossy().as_ref(),
- second_account_id,
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
- ]);
- assert!(!unapproved.0.status.success());
- assert_eq!(unapproved.1["errors"][0]["code"], "approval_required");
-
- let rebound = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "rebind",
- listing_file.to_string_lossy().as_ref(),
- second_account_id,
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
- ]);
- assert_eq!(rebound["operation_id"], "listing.rebind");
- assert_eq!(rebound["result"]["state"], "rebound");
- let after = fs::read_to_string(&listing_file).expect("listing after rebind");
- assert!(after.contains("[seller_actor]"));
- assert!(after.contains(second_account_id));
- assert!(after.contains("source = \"listing_rebind\""));
-
- let validation = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "validate",
- listing_file.to_string_lossy().as_ref(),
- ]);
- assert_eq!(validation["result"]["valid"], true);
- assert_eq!(validation["result"]["seller_account_id"], second_account_id);
- assert_eq!(validation["result"]["seller_pubkey"], second_pubkey);
-}
-
-#[test]
-fn account_id_global_populates_envelope_actor() {
- let output = radroots()
- .args([
- "--format",
- "json",
- "--account-id",
- "acct_test",
- "workspace",
- "get",
- ])
- .output()
- .expect("run workspace get");
-
- assert!(output.status.success());
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(value["operation_id"], "workspace.get");
- assert_eq!(value["actor"]["account_id"], "acct_test");
- assert_eq!(value["actor"]["role"], "account");
-}
-
-#[test]
-fn target_command_outputs_standard_json_envelope() {
- let output = radroots()
- .args(["--format", "json", "workspace", "get"])
- .output()
- .expect("run workspace get");
-
- assert!(output.status.success());
- assert!(output.stderr.is_empty());
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(value["schema_version"], "radroots.cli.output.v1");
- assert_eq!(value["operation_id"], "workspace.get");
- assert_eq!(value["kind"], "workspace.get");
- assert_eq!(value["dry_run"], false);
- assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
-}
-
-#[test]
-fn next_actions_mirror_result_actions_for_json_and_ndjson() {
- let sandbox = RadrootsCliSandbox::new();
-
- let value = sandbox.json_success(&["--format", "json", "market", "refresh"]);
-
- assert_eq!(value["result"]["actions"][0], "radroots store init");
- assert_eq!(value["next_actions"][0]["label"], "store init");
- assert_eq!(value["next_actions"][0]["command"], "radroots store init");
-
- let output = sandbox
- .command()
- .args(["--format", "ndjson", "market", "refresh"])
- .output()
- .expect("run market refresh ndjson");
- let frames = ndjson_from_stdout(&output);
- let terminal = frames.last().expect("terminal ndjson frame");
-
- assert!(output.status.success());
- assert_eq!(
- terminal["payload"]["next_actions"][0]["command"],
- "radroots store init"
- );
-}
-
-#[test]
-fn default_terminal_output_is_concise_and_not_json() {
- let output = radroots()
- .args(["workspace", "get"])
- .output()
- .expect("run workspace get");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stdout.starts_with("✓ Workspace ready\n"));
- assert!(stderr.is_empty());
- assert!(serde_json::from_str::<Value>(&stdout).is_err());
-}
-
-#[test]
-fn explicit_terminal_output_is_accepted() {
- let output = radroots()
- .args(["--format", "terminal", "workspace", "get"])
- .output()
- .expect("run terminal workspace get");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stdout.starts_with("✓ Workspace ready\n"));
- assert!(stderr.is_empty());
- assert!(serde_json::from_str::<Value>(&stdout).is_err());
-}
-
-#[test]
-fn terminal_global_presentation_flags_are_deterministic() {
- for args in [
- &["workspace", "get"][..],
- &["--quiet", "workspace", "get"][..],
- ] {
- let output = radroots().args(args).output().expect("run terminal flag");
-
- assert!(output.status.success(), "{args:?}");
- assert!(output.stderr.is_empty(), "{args:?}");
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.starts_with("✓ Workspace ready\n"), "{args:?}");
- assert!(!stdout.contains("\u{1b}["), "{args:?}");
- }
-
- for args in [
- &["--verbose", "workspace", "get"][..],
- &["--trace", "workspace", "get"][..],
- ] {
- let output = radroots().args(args).output().expect("run terminal flag");
-
- assert!(output.status.success(), "{args:?}");
- assert!(output.stderr.is_empty(), "{args:?}");
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Reference\n"), "{args:?}");
- assert!(stdout.contains("Request"), "{args:?}");
- assert!(!stdout.contains("\u{1b}["), "{args:?}");
- }
-}
-
-#[test]
-fn quiet_terminal_success_output_is_header_only() {
- let output = radroots()
- .args(["--quiet", "workspace", "get"])
- .output()
- .expect("run quiet workspace get");
-
- assert!(output.status.success());
- assert!(output.stderr.is_empty());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
-
- assert_eq!(stdout, "✓ Workspace ready\n");
-}
-
-#[test]
-fn quiet_terminal_failure_keeps_reason_and_suppresses_optional_body() {
- let output = radroots()
- .args(["--quiet", "trade", "event", "watch", "ord_missing"])
- .output()
- .expect("run quiet trade event watch");
-
- assert_eq!(output.status.code(), Some(3));
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stderr.starts_with("✕ Command failed\n"));
- assert!(stderr.contains("Reason Nostr-backed trade event watch is not implemented"));
- assert!(!stderr.contains("State not implemented"));
- assert!(!stderr.contains("Next\n"));
- assert!(serde_json::from_str::<Value>(&stderr).is_err());
-}
-
-#[test]
-fn quiet_does_not_change_json_output_contract() {
- let output = radroots()
- .args(["--format", "json", "--quiet", "workspace", "get"])
- .output()
- .expect("run quiet json workspace get");
-
- assert!(output.status.success());
- assert!(output.stderr.is_empty());
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(value["operation_id"], "workspace.get");
- assert_eq!(value["output_format"], "json");
-}
-
-#[test]
-fn removed_no_color_flag_is_rejected() {
- let output = radroots()
- .args(["--no-color", "workspace", "get"])
- .output()
- .expect("run removed no-color flag");
-
- assert_eq!(output.status.code(), Some(2));
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("unexpected argument '--no-color'"));
-}
-
-#[test]
-fn human_output_format_is_rejected() {
- let output = radroots()
- .args(["--format", "human", "workspace", "get"])
- .output()
- .expect("run human format rejection");
-
- assert_eq!(output.status.code(), Some(2));
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stderr.contains("invalid value 'human'"));
- assert!(stderr.contains("terminal"));
- assert!(stderr.contains("json"));
- assert!(stderr.contains("ndjson"));
-}
-
-#[test]
-fn environment_output_format_controls_default_rendering() {
- let sandbox = RadrootsCliSandbox::new();
- let output = sandbox
- .command()
- .env("RADROOTS_CLI_OUTPUT_FORMAT", "json")
- .args(["workspace", "get"])
- .output()
- .expect("run env json workspace get");
-
- assert!(output.status.success());
- let value = json_from_stdout(&output);
- assert_eq!(value["operation_id"], "workspace.get");
- assert_eq!(value["output_format"], "json");
-}
-
-#[test]
-fn environment_human_output_format_is_rejected() {
- let sandbox = RadrootsCliSandbox::new();
- let output = sandbox
- .command()
- .env("RADROOTS_CLI_OUTPUT_FORMAT", "human")
- .args(["workspace", "get"])
- .output()
- .expect("run env human format rejection");
-
- assert_eq!(output.status.code(), Some(2));
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stderr.starts_with("✕ Invalid input\n"));
- assert!(stderr.contains("RADROOTS_CLI_OUTPUT_FORMAT must be `terminal`, `json`, or `ndjson`"));
-}
-
-#[test]
-fn terminal_health_status_surfaces_publish_reason_and_actions() {
- let sandbox = RadrootsCliSandbox::new();
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:19007");
-
- let output = sandbox
- .command()
- .args(["health", "status", "get"])
- .output()
- .expect("run terminal health status");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
-
- assert!(stdout.starts_with("! Health needs attention\n"));
- assert!(stdout.contains("Publish unconfigured"));
- assert!(stdout.contains("Reason Nostr transport profile requires a selected or default write-capable local account for signed writes"));
- assert!(stdout.contains("Next\n radroots account create"));
- assert!(serde_json::from_str::<Value>(&stdout).is_err());
-}
-
-#[test]
-fn terminal_market_refresh_missing_store_shows_action() {
- let sandbox = RadrootsCliSandbox::new();
-
- let output = sandbox
- .command()
- .args(["market", "refresh"])
- .output()
- .expect("run terminal market refresh");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
-
- assert!(stdout.starts_with("! Market refresh unconfigured\n"));
- assert!(stdout.contains("local replica database is not initialized"));
- assert!(stdout.contains("Next\n radroots store init"));
- assert!(serde_json::from_str::<Value>(&stdout).is_err());
-}
-
-#[test]
-fn core_runtime_terminal_output_does_not_emit_secret_material() {
- let sandbox = RadrootsCliSandbox::new();
-
- for args in [
- &["config", "get"][..],
- &["account", "list"][..],
- &["account", "create"][..],
- &["signer", "status", "get"][..],
- ] {
- let output = sandbox.command().args(args).output().expect("run command");
- assert!(output.status.success(), "{args:?}");
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let lowered = stdout.to_ascii_lowercase();
-
- for forbidden in [
- "private_key",
- "secret_key",
- "seed phrase",
- "mnemonic",
- "bearer ",
- "token",
- "nsec",
- "/secrets/",
- ] {
- assert!(
- !lowered.contains(forbidden),
- "`{args:?}` terminal output leaked `{forbidden}`: {stdout}"
- );
- }
- }
-}
-
-#[test]
-fn terminal_failure_output_routes_to_stderr_and_preserves_message() {
- let output = radroots()
- .args(["--format", "terminal", "trade", "submit"])
- .output()
- .expect("run trade submit");
-
- assert_eq!(output.status.code(), Some(6));
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stderr.starts_with("✕ Approval required\n"));
- assert!(stderr.contains("Reason missing required `approval_token` input"));
- assert!(serde_json::from_str::<Value>(&stderr).is_err());
-}
-
-#[test]
-fn terminal_failure_output_renders_structured_error_detail() {
- let output = radroots()
- .args([
- "--format",
- "terminal",
- "trade",
- "event",
- "watch",
- "ord_missing",
- ])
- .output()
- .expect("run order event watch");
-
- assert_eq!(output.status.code(), Some(3));
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(stderr.starts_with("✕ Command failed\n"));
- assert!(stderr.contains("Reason Nostr-backed trade event watch is not implemented"));
- assert!(stderr.contains("State not implemented"));
- assert!(stderr.contains("Next\n radroots trade status get ord_missing"));
- assert!(serde_json::from_str::<Value>(&stderr).is_err());
-}
-
-#[test]
-fn request_ids_are_invocation_unique_and_preserve_caller_fields() {
- let first = radroots()
- .args([
- "--format",
- "json",
- "--correlation-id",
- "corr_test",
- "--idempotency-key",
- "idem_test",
- "workspace",
- "get",
- ])
- .output()
- .expect("run first workspace get");
- let second = radroots()
- .args([
- "--format",
- "json",
- "--correlation-id",
- "corr_test",
- "--idempotency-key",
- "idem_test",
- "workspace",
- "get",
- ])
- .output()
- .expect("run second workspace get");
-
- assert!(first.status.success());
- assert!(second.status.success());
- let first: Value = serde_json::from_slice(&first.stdout).expect("first json envelope");
- let second: Value = serde_json::from_slice(&second.stdout).expect("second json envelope");
-
- assert_eq!(first["correlation_id"], "corr_test");
- assert_eq!(first["idempotency_key"], "idem_test");
- assert_eq!(second["correlation_id"], "corr_test");
- assert_eq!(second["idempotency_key"], "idem_test");
- assert!(
- first["request_id"]
- .as_str()
- .expect("first request id")
- .starts_with("req_workspace_get_")
- );
- assert_ne!(first["request_id"], second["request_id"]);
-}
-
-#[test]
-fn supported_ndjson_outputs_started_and_completed_frames() {
- let sandbox = RadrootsCliSandbox::new();
- let output = sandbox
- .command()
- .args(["--format", "ndjson", "account", "list"])
- .output()
- .expect("run account list ndjson");
-
- assert!(output.status.success());
- let frames = ndjson_from_stdout(&output);
-
- assert_eq!(frames.len(), 2);
- assert_eq!(frames[0]["schema_version"], "radroots.cli.output.v1");
- assert_eq!(frames[0]["operation_id"], "account.list");
- assert_eq!(frames[0]["frame_type"], "started");
- assert_eq!(frames[0]["sequence"], 0);
- assert_eq!(frames[1]["operation_id"], "account.list");
- assert_eq!(frames[1]["frame_type"], "completed");
- assert_eq!(frames[1]["sequence"], 1);
- assert_eq!(frames[1]["errors"].as_array().expect("errors").len(), 0);
- assert_eq!(frames[0]["request_id"], frames[1]["request_id"]);
-}
-
-#[test]
-fn unsupported_ndjson_returns_structured_invalid_input() {
- let output = radroots()
- .args(["--format", "ndjson", "workspace", "get"])
- .output()
- .expect("run workspace get ndjson");
-
- assert_eq!(output.status.code(), Some(2));
- let frames = ndjson_from_stdout(&output);
-
- assert_eq!(frames.len(), 2);
- assert_eq!(frames[0]["operation_id"], "workspace.get");
- assert_eq!(frames[0]["frame_type"], "started");
- assert_eq!(frames[1]["operation_id"], "workspace.get");
- assert_eq!(frames[1]["frame_type"], "error");
- assert_eq!(frames[1]["errors"][0]["code"], "invalid_input");
- assert_eq!(frames[1]["errors"][0]["exit_code"], 2);
-
- let watch_output = radroots()
- .args([
- "--format",
- "ndjson",
- "trade",
- "event",
- "watch",
- "ord_missing",
- ])
- .output()
- .expect("run order event watch ndjson");
-
- assert_eq!(watch_output.status.code(), Some(2));
- let watch_frames = ndjson_from_stdout(&watch_output);
- assert_eq!(watch_frames.len(), 2);
- assert_eq!(watch_frames[0]["operation_id"], "trade.event.watch");
- assert_eq!(watch_frames[0]["frame_type"], "started");
- assert_eq!(watch_frames[1]["operation_id"], "trade.event.watch");
- assert_eq!(watch_frames[1]["frame_type"], "error");
- assert_eq!(watch_frames[1]["errors"][0]["code"], "invalid_input");
- assert_eq!(watch_frames[1]["errors"][0]["exit_code"], 2);
-}
-
-#[test]
-fn machine_output_exposes_status_format_resource_and_reason_code() {
- let sandbox = RadrootsCliSandbox::new();
-
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- assert_eq!(account["status"], "ok");
- assert_eq!(account["output_format"], "json");
- assert_eq!(account["reason_code"], Value::Null);
- assert_eq!(account["resource"]["kind"], "account");
- assert_eq!(
- account["resource"]["id"],
- account["result"]["account"]["id"]
- );
-
- let output = sandbox
- .command()
- .args(["--format", "json", "--dry-run", "workspace", "get"])
- .output()
- .expect("run invalid dry-run");
- assert_eq!(output.status.code(), Some(2));
- let invalid = json_from_stdout(&output);
- assert_eq!(invalid["status"], "error");
- assert_eq!(invalid["reason_code"], "invalid_input");
- assert_eq!(invalid["errors"][0]["reason_code"], "invalid_input");
-
- let ndjson_output = sandbox
- .command()
- .args(["--format", "ndjson", "--dry-run", "workspace", "get"])
- .output()
- .expect("run invalid ndjson");
- assert_eq!(ndjson_output.status.code(), Some(2));
- let frames = ndjson_from_stdout(&ndjson_output);
- assert_eq!(frames[0]["payload"]["status"], "error");
- assert_eq!(frames[0]["payload"]["output_format"], "ndjson");
- assert_eq!(frames[1]["payload"]["status"], "error");
- assert_eq!(frames[1]["payload"]["output_format"], "ndjson");
- assert_eq!(frames[1]["payload"]["reason_code"], "invalid_input");
- assert_eq!(frames[1]["errors"][0]["reason_code"], "invalid_input");
-}
-
-#[test]
-fn offline_forbids_external_network_operations() {
- for (operation_id, args) in [
- (
- "sync.pull",
- ["--format", "json", "--offline", "sync", "pull"].as_slice(),
- ),
- (
- "sync.push",
- ["--format", "json", "--offline", "sync", "push"].as_slice(),
- ),
- (
- "market.refresh",
- ["--format", "json", "--offline", "market", "refresh"].as_slice(),
- ),
- (
- "trade.submit",
- ["--format", "json", "--offline", "trade", "submit"].as_slice(),
- ),
- (
- "trade.cancel",
- [
- "--format",
- "json",
- "--offline",
- "trade",
- "cancel",
- "ord_offline_cancel",
- "--reason",
- "changed plans",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.propose",
- [
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "trade",
- "revision",
- "propose",
- "ord_offline_revision",
- "--reason",
- "update count",
- "--bin-id",
- "bin-1",
- "--bin-count",
- "2",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.accept",
- [
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "trade",
- "revision",
- "accept",
- "ord_offline_revision",
- "--revision-id",
- "revision_1",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.decline",
- [
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "trade",
- "revision",
- "decline",
- "ord_offline_revision",
- "--revision-id",
- "revision_1",
- "--reason",
- "keep original",
- ]
- .as_slice(),
- ),
- ] {
- let output = radroots()
- .args(args)
- .output()
- .expect("run offline external command");
-
- assert!(!output.status.success());
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(value["operation_id"], operation_id);
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "offline_forbidden");
- assert_eq!(value["errors"][0]["exit_code"], 8);
- }
-}
-
-#[test]
-fn offline_allows_supported_external_dry_run() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "offline-dry-run");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
-
- let publish = sandbox.json_success(&[
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert_eq!(publish["operation_id"], "listing.publish");
- assert_eq!(publish["result"]["state"], "dry_run");
-
- sandbox.json_success(&["--format", "json", "store", "init"]);
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
- let sync_push =
- sandbox.json_success(&["--format", "json", "--offline", "--dry-run", "sync", "push"]);
-
- assert_eq!(sync_push["operation_id"], "sync.push");
- assert_eq!(sync_push["result"]["state"], "ready");
-}
-
-#[test]
-fn offline_listing_publish_enqueues_sdk_outbox_without_direct_publish_push() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Offline Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag");
- let listing_file = create_listing_draft(&sandbox, "offline-sdk-enqueue");
- make_listing_publishable(&listing_file, farm_d_tag);
- let relay = "ws://127.0.0.1:9";
- let runtime_store_records_before_publish = sandbox.runtime_store_records().len();
- configure_nostr_transport(&sandbox, relay);
-
- let publish = sandbox.json_success(&[
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000104",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert_eq!(publish["operation_id"], "listing.publish");
- assert_eq!(publish["result"]["state"], "queued");
- assert_eq!(
- publish["result"]["source"],
- "SDK listing publish · configured signer"
- );
- assert_eq!(publish["result"]["target_transport_endpoints"][0], relay);
- assert_eq!(publish["result"]["actions"][0], "radroots sync push");
- assert_eq!(
- publish["result"]["event_id"]
- .as_str()
- .expect("sdk event id")
- .len(),
- 64
- );
- assert!(
- sandbox
- .root()
- .join("data/apps/cli/replica/sdk/runtime.sqlite")
- .exists()
- );
- assert_eq!(
- sandbox.runtime_store_records().len(),
- runtime_store_records_before_publish
- );
-}
-
-#[test]
-fn listing_publish_idempotency_conflict_maps_sdk_partial_mutation_recovery() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Conflict Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag");
- let listing_file = create_listing_draft(&sandbox, "idem-conflict");
- make_listing_publishable(&listing_file, farm_d_tag);
- let relay = "ws://127.0.0.1:9";
- let idempotency_key = "01890f3c-7f2a-7b01-8000-000000000102";
- configure_nostr_transport(&sandbox, relay);
-
- sandbox.json_success(&[
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "--idempotency-key",
- idempotency_key,
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
- let raw = fs::read_to_string(&listing_file).expect("listing draft");
- fs::write(
- &listing_file,
- raw.replace("title = \"Eggs\"", "title = \"Conflict Eggs\""),
- )
- .expect("rewrite listing draft");
-
- let (output, conflict) = sandbox.json_output(&[
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "--idempotency-key",
- idempotency_key,
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(conflict["operation_id"], "listing.publish");
- assert_eq!(conflict["errors"][0]["code"], "idempotency_conflict");
- assert_eq!(conflict["errors"][0]["detail"]["class"], "request");
- assert_eq!(
- conflict["errors"][0]["detail"]["actions"][0],
- "radroots listing publish"
- );
-}
-
-#[test]
-fn offline_rejects_order_decision_dry_run() {
- for (operation_id, args) in [
- (
- "trade.accept",
- [
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "trade",
- "accept",
- "ord_offline_decision",
- ]
- .as_slice(),
- ),
- (
- "trade.decline",
- [
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "trade",
- "decline",
- "ord_offline_decision",
- "--reason",
- "unavailable",
- ]
- .as_slice(),
- ),
- (
- "trade.cancel",
- [
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "trade",
- "cancel",
- "ord_offline_decision",
- "--reason",
- "changed plans",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.propose",
- [
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "trade",
- "revision",
- "propose",
- "ord_offline_revision",
- "--reason",
- "update count",
- "--bin-id",
- "bin-1",
- "--bin-count",
- "2",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.accept",
- [
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "trade",
- "revision",
- "accept",
- "ord_offline_revision",
- "--revision-id",
- "revision_1",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.decline",
- [
- "--format",
- "json",
- "--offline",
- "--dry-run",
- "trade",
- "revision",
- "decline",
- "ord_offline_revision",
- "--revision-id",
- "revision_1",
- "--reason",
- "keep original",
- ]
- .as_slice(),
- ),
- ] {
- let output = radroots()
- .args(args)
- .output()
- .expect("run offline order decision dry-run");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(output.status.code(), Some(8));
- assert_eq!(value["operation_id"], operation_id);
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "offline_forbidden");
- assert_eq!(value["errors"][0]["exit_code"], 8);
- }
-}
-
-#[test]
-fn listing_publish_dry_run_validates_missing_file() {
- let sandbox = RadrootsCliSandbox::new();
- let missing = sandbox.root().join("missing-listing.toml");
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "publish",
- missing.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "not_found");
- assert_eq!(value["errors"][0]["exit_code"], 4);
- assert_no_removed_command_reference(
- &value,
- &["listing", "publish", "--dry-run", "missing-listing.toml"],
- );
-}
-
-#[test]
-fn listing_publish_invalid_draft_returns_validation_failure() {
- let sandbox = RadrootsCliSandbox::new();
- let invalid = sandbox.root().join("invalid-listing.toml");
- fs::write(&invalid, "listing = [").expect("write invalid listing");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "publish",
- invalid.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "validation_failed");
- assert_eq!(value["errors"][0]["exit_code"], 10);
-}
-
-#[test]
-fn online_requires_relay_for_external_network_operations() {
- for (operation_id, args) in [
- (
- "sync.pull",
- ["--format", "json", "--online", "sync", "pull"].as_slice(),
- ),
- (
- "sync.push",
- ["--format", "json", "--online", "sync", "push"].as_slice(),
- ),
- (
- "market.refresh",
- ["--format", "json", "--online", "market", "refresh"].as_slice(),
- ),
- (
- "trade.event.list",
- ["--format", "json", "--online", "trade", "event", "list"].as_slice(),
- ),
- (
- "trade.cancel",
- [
- "--format",
- "json",
- "--online",
- "trade",
- "cancel",
- "ord_missing",
- "--reason",
- "changed plans",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.propose",
- [
- "--format",
- "json",
- "--online",
- "--approval-token",
- "approve",
- "trade",
- "revision",
- "propose",
- "ord_missing",
- "--reason",
- "update count",
- "--bin-id",
- "bin-1",
- "--bin-count",
- "2",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.accept",
- [
- "--format",
- "json",
- "--online",
- "--dry-run",
- "trade",
- "revision",
- "accept",
- "ord_missing",
- "--revision-id",
- "revision_1",
- ]
- .as_slice(),
- ),
- (
- "trade.revision.decline",
- [
- "--format",
- "json",
- "--online",
- "--dry-run",
- "trade",
- "revision",
- "decline",
- "ord_missing",
- "--revision-id",
- "revision_1",
- "--reason",
- "keep original",
- ]
- .as_slice(),
- ),
- ] {
- let output = radroots()
- .args(args)
- .output()
- .expect("run online external command");
-
- assert!(!output.status.success());
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
-
- assert_eq!(value["operation_id"], operation_id);
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable");
- assert_eq!(value["errors"][0]["exit_code"], 8);
- assert!(
- value["errors"][0]["message"]
- .as_str()
- .expect("message")
- .contains("requires a delivery-capable transport profile")
- );
- }
-}
-
-#[test]
-fn order_status_get_uses_sdk_local_projection_without_relay_fetch() {
- let sandbox = RadrootsCliSandbox::new();
- let local = sandbox.json_success(&[
- "--format",
- "json",
- "--online",
- "trade",
- "status",
- "get",
- "ord_missing",
- ]);
-
- assert_eq!(local["operation_id"], "trade.status.get");
- assert_eq!(local["result"]["state"], "missing");
- assert_eq!(local["result"]["source"], "SDK local trade projection");
- assert_eq!(
- local["result"]["actor_context_source"],
- "sdk_local_projection"
- );
- assert_eq!(local["result"]["fetched_count"], 0);
- assert_eq!(local["result"]["decoded_count"], 0);
-
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind closed relay");
- let closed_relay = format!("ws://{}", listener.local_addr().expect("relay addr"));
- drop(listener);
- configure_nostr_transport(&sandbox, closed_relay.as_str());
- let with_closed_relay =
- sandbox.json_success(&["--format", "json", "trade", "status", "get", "ord_missing"]);
-
- assert_eq!(with_closed_relay["operation_id"], "trade.status.get");
- assert_eq!(with_closed_relay["result"]["state"], "missing");
- assert_eq!(
- with_closed_relay["result"]["source"],
- "SDK local trade projection"
- );
- assert_eq!(with_closed_relay["result"]["fetched_count"], 0);
- assert_eq!(with_closed_relay["result"]["decoded_count"], 0);
-}
-
-#[test]
-fn order_status_get_invalid_order_id_uses_sdk_error_contract() {
- let sandbox = RadrootsCliSandbox::new();
- let (output, value) =
- sandbox.json_output(&["--format", "json", "trade", "status", "get", "bad order id"]);
-
- assert!(!output.status.success());
- assert_eq!(value["operation_id"], "trade.status.get");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "invalid_trade_id");
- assert_eq!(value["errors"][0]["exit_code"], 2);
- assert_eq!(value["errors"][0]["detail"]["class"], "request");
- assert_eq!(value["errors"][0]["detail"]["retryable"], false);
- assert_eq!(
- value["errors"][0]["detail"]["detail"]["value"],
- "bad order id"
- );
-}
-
-#[test]
-fn removed_transport_profile_flag_is_rejected() {
- let sandbox = RadrootsCliSandbox::new();
- let output = sandbox
- .command()
- .args(["--publish-transport", "radrootsd", "sync", "push"])
- .output()
- .expect("run removed publish transport flag");
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
-
- assert!(!output.status.success());
- assert!(stderr.contains("unexpected argument") || stderr.contains("unrecognized"));
-}
-
-#[test]
-fn online_order_event_watch_returns_deferred_without_relay_preflight() {
- let sandbox = RadrootsCliSandbox::new();
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--online",
- "trade",
- "event",
- "watch",
- "ord_missing",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "trade.event.watch");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "not_implemented");
- assert_eq!(value["errors"][0]["detail"]["state"], "not_implemented");
- assert_eq!(value["errors"][0]["detail"]["trade_id"], "ord_missing");
- assert_eq!(
- value["next_actions"][0]["command"],
- "radroots trade status get ord_missing"
- );
- assert!(
- !value["errors"][0]["message"]
- .as_str()
- .expect("message")
- .contains("configured Nostr relay")
- );
- assert_no_daemon_runtime_reference(&value, &["trade", "event", "watch"]);
-}
-
-#[test]
-fn online_allows_local_diagnostics() {
- let value = RadrootsCliSandbox::new().json_success(&[
- "--format",
- "json",
- "--online",
- "workspace",
- "get",
- ]);
-
- assert_eq!(value["operation_id"], "workspace.get");
- assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
-}
-
-#[test]
-fn store_export_dry_run_is_structured_unsupported() {
- let sandbox = RadrootsCliSandbox::new();
- let (output, value) =
- sandbox.json_output(&["--format", "json", "--dry-run", "store", "export"]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2));
- assert_eq!(value["operation_id"], "store.export");
- assert_eq!(value["errors"][0]["code"], "invalid_input");
- assert_eq!(value["errors"][0]["exit_code"], 2);
-}
-
-#[test]
-fn store_backup_and_restore_use_sdk_canonical_store() {
- let sandbox = RadrootsCliSandbox::new();
- let sdk_root = sandbox.root().join("data/apps/cli/replica/sdk");
-
- assert!(!sdk_root.exists());
-
- let status = sandbox.json_success(&["--format", "json", "store", "status", "get"]);
-
- assert_eq!(status["operation_id"], "store.status.get");
- assert_eq!(status["result"]["state"], "ready");
- assert_eq!(
- status["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(status["result"]["canonical_store"], "sdk");
- assert_eq!(status["result"]["sdk_storage"], "directory");
- assert_eq!(status["result"]["sdk_existed_before_open"], false);
- assert_eq!(
- status["result"]["event_store"]["store"]["integrity_ok"],
- true
- );
- assert_eq!(status["result"]["outbox"]["store"]["integrity_ok"], true);
- assert_eq!(
- status["result"]["derived_projection"]["state"],
- "unconfigured"
- );
- assert_eq!(
- status["result"]["derived_projection"]["source"],
- "local derived projection cache"
- );
- assert!(sdk_root.join("runtime.sqlite").exists());
-
- let derived_projection_init = sandbox.json_success(&["--format", "json", "store", "init"]);
- assert_eq!(derived_projection_init["operation_id"], "store.init");
-
- let status_after_projection =
- sandbox.json_success(&["--format", "json", "store", "status", "get"]);
-
- assert_eq!(
- status_after_projection["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(
- status_after_projection["result"]["derived_projection"]["state"],
- "ready"
- );
- assert_eq!(
- status_after_projection["result"]["derived_projection"]["source"],
- "local derived projection cache"
- );
-
- let dry_run =
- sandbox.json_success(&["--format", "json", "--dry-run", "store", "backup", "create"]);
- let dry_run_destination = dry_run["result"]["destination"]
- .as_str()
- .expect("backup destination");
- let dry_run_file = dry_run["result"]["file"].as_str().expect("backup file");
-
- assert_eq!(dry_run["operation_id"], "store.backup.create");
- assert_eq!(dry_run["dry_run"], true);
- assert_eq!(dry_run["result"]["state"], "dry_run");
- assert_eq!(
- dry_run["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(dry_run["result"]["backup_kind"], "sdk_canonical");
- assert_eq!(dry_run["result"]["canonical_store"], "sdk");
- assert_eq!(dry_run["result"]["size_bytes"], 0);
- assert_eq!(
- dry_run["result"]["manifest"]["manifest_kind"],
- "sdk_canonical_backup_preview"
- );
- assert_eq!(
- dry_run["result"]["manifest"]["backup_verification"]["event_store_ok"],
- true
- );
- assert_eq!(
- dry_run["result"]["manifest"]["backup_verification"]["outbox_ok"],
- true
- );
- assert!(!Path::new(dry_run_destination).exists());
- assert!(!Path::new(dry_run_file).exists());
-
- let backup = sandbox.json_success(&["--format", "json", "store", "backup", "create"]);
- let backup_destination = backup["result"]["destination"]
- .as_str()
- .expect("backup destination");
- let event_store_file = backup["result"]["event_store_file"]
- .as_str()
- .expect("event store backup");
- let outbox_file = backup["result"]["outbox_file"]
- .as_str()
- .expect("outbox backup");
- let manifest_file = backup["result"]["manifest_file"]
- .as_str()
- .expect("manifest backup");
-
- assert_eq!(backup["operation_id"], "store.backup.create");
- assert_eq!(backup["result"]["state"], "completed");
- assert_eq!(
- backup["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(backup["result"]["backup_kind"], "sdk_canonical");
- assert_eq!(backup["result"]["canonical_store"], "sdk");
- assert_eq!(
- backup["result"]["manifest"]["manifest_kind"],
- "storage_backup"
- );
- assert!(
- backup["result"]["size_bytes"]
- .as_u64()
- .expect("backup size")
- > 0
- );
- assert!(Path::new(backup_destination).exists());
- assert!(Path::new(event_store_file).exists());
- assert!(Path::new(outbox_file).exists());
- assert!(Path::new(manifest_file).exists());
- assert_eq!(
- backup["result"]["manifest"]["backup_verification"]["event_store_ok"],
- true
- );
- assert_eq!(
- backup["result"]["manifest"]["backup_verification"]["outbox_ok"],
- true
- );
- assert!(
- backup["result"]["manifest"]["source_paths"]["runtime_path"]
- .as_str()
- .expect("source runtime path")
- .contains("data/apps/cli/replica/sdk/runtime.sqlite")
- );
- assert!(!event_store_file.ends_with("replica.sqlite"));
-
- let restore_destination = sandbox.root().join("restored-sdk-store");
- let restore_destination_arg = restore_destination.to_string_lossy().to_string();
- let restore_dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "store",
- "backup",
- "restore",
- backup_destination,
- "--destination",
- restore_destination_arg.as_str(),
- ]);
-
- assert_eq!(restore_dry_run["operation_id"], "store.backup.restore");
- assert_eq!(restore_dry_run["dry_run"], true);
- assert_eq!(restore_dry_run["result"]["state"], "dry_run");
- assert_eq!(
- restore_dry_run["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(restore_dry_run["result"]["restore_kind"], "sdk_canonical");
- assert_eq!(restore_dry_run["result"]["canonical_store"], "sdk");
- assert_eq!(
- restore_dry_run["result"]["backup_source"],
- backup_destination
- );
- assert_eq!(
- restore_dry_run["result"]["destination"],
- restore_destination_arg
- );
- assert_eq!(
- restore_dry_run["result"]["manifest"]["manifest_kind"],
- "storage_backup"
- );
- assert_eq!(
- restore_dry_run["result"]["verification"]["event_store_ok"],
- true
- );
- assert_eq!(restore_dry_run["result"]["verification"]["outbox_ok"], true);
- assert!(!restore_destination.exists());
-
- let restored = sandbox.json_success(&[
- "--format",
- "json",
- "store",
- "backup",
- "restore",
- backup_destination,
- "--destination",
- restore_destination_arg.as_str(),
- ]);
-
- assert_eq!(restored["operation_id"], "store.backup.restore");
- assert_eq!(restored["result"]["state"], "completed");
- assert_eq!(
- restored["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(restored["result"]["restore_kind"], "sdk_canonical");
- assert_eq!(restored["result"]["canonical_store"], "sdk");
- assert!(restore_destination.join("runtime.sqlite").exists());
- assert_eq!(
- restored["result"]["restored_event_store_file"],
- restore_destination
- .join("runtime.sqlite")
- .to_string_lossy()
- .to_string()
- );
- assert_eq!(
- restored["result"]["restored_outbox_file"],
- restore_destination
- .join("runtime.sqlite")
- .to_string_lossy()
- .to_string()
- );
-
- let unapproved_overwrite = sandbox.json_output(&[
- "--format",
- "json",
- "store",
- "backup",
- "restore",
- backup_destination,
- "--destination",
- restore_destination_arg.as_str(),
- "--overwrite",
- ]);
- assert!(!unapproved_overwrite.0.status.success());
- assert_eq!(unapproved_overwrite.0.status.code(), Some(6));
- assert_eq!(
- unapproved_overwrite.1["operation_id"],
- "store.backup.restore"
- );
- assert_eq!(
- unapproved_overwrite.1["errors"][0]["code"],
- "approval_required"
- );
-
- let approved_overwrite = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "restore-ok",
- "store",
- "backup",
- "restore",
- backup_destination,
- "--destination",
- restore_destination_arg.as_str(),
- "--overwrite",
- ]);
- assert_eq!(approved_overwrite["operation_id"], "store.backup.restore");
- assert_eq!(approved_overwrite["result"]["state"], "completed");
- assert_eq!(approved_overwrite["result"]["overwrite"], true);
-
- let missing_backup = sandbox.root().join("missing-sdk-backup");
- let missing_backup_arg = missing_backup.to_string_lossy().to_string();
- let (missing_output, missing_value) = sandbox.json_output(&[
- "--format",
- "json",
- "store",
- "backup",
- "restore",
- missing_backup_arg.as_str(),
- "--destination",
- sandbox
- .root()
- .join("missing-restore-destination")
- .to_string_lossy()
- .as_ref(),
- ]);
- assert!(!missing_output.status.success());
- assert_eq!(missing_value["operation_id"], "store.backup.restore");
- assert_eq!(missing_value["errors"][0]["code"], "io");
- assert_eq!(missing_value["errors"][0]["detail"]["class"], "storage");
-}
-
-#[test]
-fn core_account_store_dry_runs_preflight_without_mutating_local_state() {
- let sandbox = RadrootsCliSandbox::new();
-
- let workspace = sandbox.json_success(&["--format", "json", "--dry-run", "workspace", "init"]);
- let workspace_db = workspace["result"]["local"]["path"]
- .as_str()
- .expect("workspace db path");
- assert_eq!(workspace["operation_id"], "workspace.init");
- assert_eq!(workspace["dry_run"], true);
- assert_eq!(workspace["result"]["state"], "dry_run");
- assert_eq!(workspace["result"]["local"]["replica_store"], "missing");
- assert!(!Path::new(workspace_db).exists());
-
- let store = sandbox.json_success(&["--format", "json", "--dry-run", "store", "init"]);
- let store_db = store["result"]["path"].as_str().expect("store db path");
- assert_eq!(store["operation_id"], "store.init");
- assert_eq!(store["dry_run"], true);
- assert_eq!(store["result"]["state"], "dry_run");
- assert_eq!(store["result"]["replica_store"], "missing");
- assert!(!Path::new(store_db).exists());
-
- let account_create =
- sandbox.json_success(&["--format", "json", "--dry-run", "account", "create"]);
- assert_eq!(account_create["operation_id"], "account.create");
- assert_eq!(account_create["dry_run"], true);
- assert_eq!(account_create["result"]["state"], "dry_run");
- assert_eq!(account_create["result"]["secret_backend"]["state"], "ready");
-
- let account_list = sandbox.json_success(&["--format", "json", "account", "list"]);
- assert_eq!(account_list["result"]["count"], 0);
-
- let created = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = created["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let clear = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "account",
- "selection",
- "clear",
- ]);
- assert_eq!(clear["operation_id"], "account.selection.clear");
- assert_eq!(clear["result"]["state"], "dry_run");
- assert_eq!(clear["result"]["cleared_account"]["id"], account_id);
- assert_eq!(clear["result"]["remaining_account_count"], 1);
-
- let selection = sandbox.json_success(&["--format", "json", "account", "selection", "get"]);
- assert_eq!(
- selection["result"]["account_resolution"]["default_account"]["id"],
- account_id
- );
-}
-
-#[test]
-fn seller_dry_runs_preflight_without_mutating_farm_or_listing_files() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
-
- let farm_dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_path = farm_dry_run["result"]["config"]["path"]
- .as_str()
- .expect("farm path");
- assert_eq!(farm_dry_run["operation_id"], "farm.create");
- assert_eq!(farm_dry_run["result"]["state"], "dry_run");
- assert!(!Path::new(farm_path).exists());
-
- let missing_update = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "profile",
- "update",
- "--value",
- "Dry Name",
- ]);
- assert_eq!(missing_update["operation_id"], "farm.profile.update");
- assert_eq!(missing_update["result"]["state"], "unconfigured");
- assert!(!Path::new(farm_path).exists());
-
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_path = farm["result"]["config"]["path"]
- .as_str()
- .expect("farm path");
- let farm_before = fs::read_to_string(farm_path).expect("farm before");
- let farm_update = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "profile",
- "update",
- "--value",
- "Dry Name",
- ]);
- assert_eq!(farm_update["operation_id"], "farm.profile.update");
- assert_eq!(farm_update["result"]["state"], "dry_run");
- assert_eq!(farm_update["result"]["config"]["name"], "Dry Name");
- assert_eq!(
- fs::read_to_string(farm_path).expect("farm after dry-run"),
- farm_before
- );
-
- let listing_path = sandbox.root().join("dry-listing.toml");
- let listing_path_arg = listing_path.to_string_lossy();
- let listing_dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "create",
- "--output",
- listing_path_arg.as_ref(),
- "--key",
- "eggs",
- "--title",
- "Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- assert_eq!(listing_dry_run["operation_id"], "listing.create");
- assert_eq!(listing_dry_run["result"]["state"], "dry_run");
- assert_eq!(listing_dry_run["result"]["file"], listing_path_arg.as_ref());
- assert!(!listing_path.exists());
-
- fs::write(&listing_path, "existing").expect("existing listing path");
- let (collision_output, collision) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "create",
- "--output",
- listing_path_arg.as_ref(),
- "--key",
- "eggs",
- ]);
- assert!(!collision_output.status.success());
- assert_eq!(collision["operation_id"], "listing.create");
- assert_eq!(collision["errors"][0]["code"], "validation_failed");
-
- let listing_file = create_listing_draft(&sandbox, "seller-dry-run");
- make_listing_publishable(
- &listing_file,
- farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag"),
- );
- let listing_before = fs::read_to_string(&listing_file).expect("listing before");
- let listing_update = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "update",
- listing_file.to_string_lossy().as_ref(),
- ]);
- assert_eq!(listing_update["operation_id"], "listing.update");
- assert_eq!(listing_update["result"]["state"], "dry_run");
- assert_eq!(
- fs::read_to_string(&listing_file).expect("listing after dry-run"),
- listing_before
- );
-}
-
-#[test]
-fn seller_dry_runs_do_not_write_shared_local_work_records() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
-
- sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "farm",
- "create",
- "--name",
- "Dry Run Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- assert!(sandbox.runtime_store_records().is_empty());
-
- let listing_path = sandbox.root().join("dry-run-local-work.toml");
- let listing_path_arg = listing_path.to_string_lossy();
- sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "create",
- "--output",
- listing_path_arg.as_ref(),
- "--key",
- "dry-run-eggs",
- "--title",
- "Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- assert!(sandbox.runtime_store_records().is_empty());
-}
-
-#[test]
-fn seller_local_writes_append_shared_local_work_records() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_config = &farm["result"]["config"];
- let farm_d_tag = farm_config["farm_d_tag"].as_str().expect("farm d tag");
- let seller_pubkey = farm_config["seller_pubkey"]
- .as_str()
- .expect("seller pubkey");
- let listing_file = create_listing_draft(&sandbox, "shared-local-eggs");
-
- let records = sandbox.runtime_store_records();
- assert_eq!(records.len(), 2);
-
- let farm_record = records
- .iter()
- .find(|record| {
- record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload["record_kind"].as_str())
- == Some("farm_config_v1")
- })
- .expect("farm local work record");
- assert_eq!(farm_record.family, RuntimeStoreRecordFamily::LocalWork);
- assert_eq!(farm_record.status, RuntimeStoreRecordStatus::LocalSaved);
- assert_eq!(farm_record.source_runtime, SourceRuntime::Cli);
- assert_eq!(farm_record.outbox_status, PublishOutboxStatus::None);
- assert_eq!(farm_record.owner_account_id.as_deref(), Some(account_id));
- assert_eq!(farm_record.owner_pubkey.as_deref(), Some(seller_pubkey));
- assert_eq!(farm_record.farm_id.as_deref(), Some(farm_d_tag));
- assert_eq!(farm_record.listing_addr, None);
- let farm_payload = farm_record
- .local_work_json
- .as_ref()
- .expect("farm local work payload");
- assert_eq!(farm_payload["scope"], "workspace");
- assert_eq!(farm_payload["document"]["farm"]["d_tag"], farm_d_tag);
-
- let listing_record = records
- .iter()
- .find(|record| {
- record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload["record_kind"].as_str())
- == Some("listing_draft_v1")
- })
- .expect("listing local work record");
- assert_eq!(listing_record.family, RuntimeStoreRecordFamily::LocalWork);
- assert_eq!(listing_record.status, RuntimeStoreRecordStatus::LocalSaved);
- assert_eq!(listing_record.source_runtime, SourceRuntime::Cli);
- assert_eq!(listing_record.outbox_status, PublishOutboxStatus::None);
- assert_eq!(listing_record.owner_account_id.as_deref(), Some(account_id));
- assert_eq!(listing_record.owner_pubkey.as_deref(), Some(seller_pubkey));
- assert_eq!(listing_record.farm_id.as_deref(), Some(farm_d_tag));
- assert!(
- listing_record
- .listing_addr
- .as_deref()
- .expect("listing addr")
- .starts_with(format!("30402:{seller_pubkey}:").as_str())
- );
- let listing_payload = listing_record
- .local_work_json
- .as_ref()
- .expect("listing local work payload");
- assert_eq!(listing_payload["path"], listing_file.display().to_string());
- assert_eq!(
- listing_payload["document"]["product"]["key"],
- "shared-local-eggs"
- );
-
- let farm_update = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "profile",
- "update",
- "--value",
- "Green Farm Updated",
- ]);
- assert_eq!(farm_update["operation_id"], "farm.profile.update");
- assert_eq!(farm_update["result"]["state"], "updated");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "rebind",
- listing_file.to_string_lossy().as_ref(),
- second_account_id,
- "--farm-d-tag",
- farm_d_tag,
- ]);
-
- let updated_records = sandbox.runtime_store_records();
- assert_eq!(updated_records.len(), 4);
- let latest_farm_payload = updated_records
- .iter()
- .filter(|record| {
- record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload["record_kind"].as_str())
- == Some("farm_config_v1")
- })
- .max_by_key(|record| record.seq)
- .and_then(|record| record.local_work_json.as_ref())
- .expect("latest farm payload");
- assert_eq!(
- latest_farm_payload["document"]["profile"]["name"],
- "Green Farm Updated"
- );
- let latest_listing = updated_records
- .iter()
- .filter(|record| {
- record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload["record_kind"].as_str())
- == Some("listing_draft_v1")
- })
- .max_by_key(|record| record.seq)
- .expect("latest listing record");
- assert_eq!(
- latest_listing.owner_account_id.as_deref(),
- Some(second_account_id)
- );
- let latest_listing_payload = latest_listing
- .local_work_json
- .as_ref()
- .expect("latest listing payload");
- assert_eq!(
- latest_listing_payload["document"]["seller_actor"]["account_id"],
- second_account_id
- );
-}
-
-#[test]
-fn listing_app_records_list_and_export_to_valid_cli_draft() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag);
- let listing_record_id = seed_app_listing_record(
- &sandbox,
- account_id,
- seller_pubkey,
- farm_d_tag,
- listing_d_tag,
- );
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["operation_id"], "listing.app.list");
- assert_eq!(list["result"]["state"], "ready");
- assert_eq!(list["result"]["count"], 2);
- let listing_row = list["result"]["records"]
- .as_array()
- .expect("records")
- .iter()
- .find(|record| record["record_id"] == listing_record_id)
- .expect("listing row");
- assert_eq!(listing_row["record_kind"], "listing_draft_v1");
- assert_eq!(listing_row["source_runtime"], "app");
- assert_eq!(listing_row["exportable"], true);
- assert_eq!(listing_row["listing_id"], listing_d_tag);
- assert_eq!(listing_row["title"], "App Eggs");
-
- let export_path = sandbox.root().join("app-eggs.toml");
- let export_path_arg = export_path.to_string_lossy();
- let dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "app",
- "export",
- listing_record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert_eq!(dry_run["operation_id"], "listing.app.export");
- assert_eq!(dry_run["result"]["state"], "dry_run");
- assert_eq!(dry_run["result"]["valid"], true);
- assert!(!export_path.exists());
-
- let export = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "app",
- "export",
- listing_record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert_eq!(export["operation_id"], "listing.app.export");
- assert_eq!(export["result"]["state"], "exported");
- assert_eq!(export["result"]["listing_id"], listing_d_tag);
- assert_eq!(export["result"]["seller_account_id"], account_id);
- assert!(export_path.exists());
- let exported_contents = fs::read_to_string(&export_path).expect("exported listing draft");
- assert!(exported_contents.contains("quantity_unit = \"each\""));
- assert!(exported_contents.contains("price_per_unit = \"each\""));
- assert!(exported_contents.contains("label = \"dozen\""));
-
- let validate = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "validate",
- export_path_arg.as_ref(),
- ]);
- assert_eq!(validate["operation_id"], "listing.validate");
- assert_eq!(validate["result"]["valid"], true);
- assert_eq!(validate["result"]["listing_id"], listing_d_tag);
- assert_eq!(validate["result"]["seller_account_id"], account_id);
-}
-
-#[test]
-fn listing_app_records_list_current_records_and_blocks_stale_export() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag);
- let stale_record_id = seed_app_listing_record_variant(
- &sandbox,
- account_id,
- Some(seller_pubkey),
- farm_d_tag,
- listing_d_tag,
- "stale",
- "Old App Eggs",
- None,
- );
- let current_record_id = seed_app_listing_record_variant(
- &sandbox,
- account_id,
- Some(seller_pubkey),
- farm_d_tag,
- listing_d_tag,
- "current",
- "Current App Eggs",
- None,
- );
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["result"]["count"], 2);
- assert_eq!(list["result"]["limit"], 500);
- assert_eq!(list["result"]["has_more"], false);
- let records = list["result"]["records"].as_array().expect("records");
- assert!(
- records
- .iter()
- .all(|record| record["record_id"] != stale_record_id)
- );
- let listing_row = records
- .iter()
- .find(|record| record["record_id"] == current_record_id)
- .expect("current listing row");
- assert_eq!(listing_row["title"], "Current App Eggs");
- assert_eq!(listing_row["superseded_count"], 1);
- assert_eq!(listing_row["exportable"], true);
- assert!(
- listing_row["change_seq"]
- .as_i64()
- .expect("listing change seq")
- > records[1]["change_seq"].as_i64().expect("farm change seq")
- );
-
- let export_path = sandbox.root().join("stale-app-eggs.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (output, stale_export) = sandbox.json_output(&[
- "--format",
- "json",
- "listing",
- "app",
- "export",
- stale_record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!output.status.success());
- assert_eq!(stale_export["result"], Value::Null);
- assert_eq!(stale_export["errors"][0]["detail"]["state"], "stale");
- assert_eq!(stale_export["errors"][0]["detail"]["valid"], false);
- assert!(
- stale_export["errors"][0]["message"]
- .as_str()
- .expect("stale reason")
- .contains(current_record_id.as_str())
- );
- assert!(!export_path.exists());
-}
-
-#[test]
-fn listing_app_records_list_includes_new_records_after_older_volume() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- for index in 0..505 {
- let farm_d_tag = format!("F{index:021}");
- seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag.as_str());
- }
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let current_record_id = seed_app_listing_record_variant(
- &sandbox,
- account_id,
- Some(seller_pubkey),
- "AAAAAAAAAAAAAAAAAAAAAw",
- listing_d_tag,
- "current",
- "Newest App Eggs",
- None,
- );
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["result"]["limit"], 500);
- assert_eq!(list["result"]["count"], 500);
- assert_eq!(list["result"]["has_more"], true);
- assert!(list["result"]["next_before_change_seq"].as_i64().is_some());
- assert!(list["result"]["next_before_seq"].as_i64().is_some());
- let records = list["result"]["records"].as_array().expect("records");
- assert_eq!(records[0]["record_id"], current_record_id);
- assert!(
- records
- .iter()
- .any(|record| record["record_id"] == current_record_id)
- );
-}
-
-#[test]
-fn listing_app_records_keep_same_listing_id_separate_by_owner_pubkey() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- let other_pubkey = identity_public(83).public_key_hex;
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let first_record_id = seed_app_listing_record_variant_without_listing_addr(
- &sandbox,
- account_id,
- Some(seller_pubkey),
- farm_d_tag,
- listing_d_tag,
- "owner-one",
- "First Owner Eggs",
- );
- let second_record_id = seed_app_listing_record_variant_without_listing_addr(
- &sandbox,
- "acct_owner_two",
- Some(other_pubkey.as_str()),
- farm_d_tag,
- listing_d_tag,
- "owner-two",
- "Second Owner Eggs",
- );
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["result"]["count"], 2);
- let records = list["result"]["records"].as_array().expect("records");
- let first_row = records
- .iter()
- .find(|record| record["record_id"] == first_record_id)
- .expect("first owner row");
- let second_row = records
- .iter()
- .find(|record| record["record_id"] == second_record_id)
- .expect("second owner row");
- assert_eq!(first_row["title"], "First Owner Eggs");
- assert_eq!(second_row["title"], "Second Owner Eggs");
- assert_eq!(first_row["superseded_count"], 0);
- assert_eq!(second_row["superseded_count"], 0);
- assert_eq!(first_row["exportable"], true);
- assert_eq!(second_row["exportable"], true);
-}
-
-#[test]
-fn listing_app_records_export_blocks_stale_when_current_is_beyond_first_page() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let stale_record_id = seed_app_listing_record_variant(
- &sandbox,
- account_id,
- Some(seller_pubkey),
- farm_d_tag,
- listing_d_tag,
- "paged-stale",
- "Paged Old Eggs",
- None,
- );
- let current_record_id = seed_app_listing_record_variant(
- &sandbox,
- account_id,
- Some(seller_pubkey),
- farm_d_tag,
- listing_d_tag,
- "paged-current",
- "Paged Current Eggs",
- None,
- );
- for index in 0..505 {
- let farm_d_tag = format!("G{index:021}");
- seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag.as_str());
- }
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["result"]["count"], 500);
- assert_eq!(list["result"]["has_more"], true);
- let records = list["result"]["records"].as_array().expect("records");
- assert!(
- records
- .iter()
- .all(|record| record["record_id"] != current_record_id)
- );
-
- let export_path = sandbox.root().join("paged-stale-app-eggs.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (output, stale_export) = sandbox.json_output(&[
- "--format",
- "json",
- "listing",
- "app",
- "export",
- stale_record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!output.status.success());
- assert_eq!(stale_export["result"], Value::Null);
- assert_eq!(stale_export["errors"][0]["detail"]["state"], "stale");
- assert!(
- stale_export["errors"][0]["message"]
- .as_str()
- .expect("stale reason")
- .contains(current_record_id.as_str())
- );
- assert!(!export_path.exists());
-}
-
-#[test]
-fn listing_app_records_mark_unresolved_pubkey_records_non_exportable() {
- let sandbox = RadrootsCliSandbox::new();
- let account_id = "acct_unresolved";
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let record_id = seed_app_listing_record_variant(
- &sandbox,
- account_id,
- None,
- farm_d_tag,
- listing_d_tag,
- "unresolved",
- "Unresolved App Eggs",
- Some(json!({
- "state": "identity_unresolved",
- "reason": "canonical_hex_pubkey_required"
- })),
- );
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["result"]["count"], 1);
- let listing_row = &list["result"]["records"][0];
- assert_eq!(listing_row["record_id"], record_id);
- assert_eq!(listing_row["title"], "Unresolved App Eggs");
- assert_eq!(listing_row["exportable"], false);
- assert_eq!(
- listing_row["reason"],
- "canonical hex pubkey required before export"
- );
- assert!(listing_row.get("listing_addr").is_none());
-
- let export_path = sandbox.root().join("unresolved-app-eggs.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "listing",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!output.status.success());
- assert_eq!(export["result"], Value::Null);
- assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
- assert_eq!(
- export["errors"][0]["message"],
- "canonical hex pubkey required before export"
- );
- assert!(!export_path.exists());
-}
-
-#[test]
-fn listing_app_records_ignore_body_pubkey_without_owner_metadata() {
- let sandbox = RadrootsCliSandbox::new();
- let account_id = "acct_body_only";
- let body_pubkey = identity_public(91).public_key_hex;
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let record_id = seed_app_listing_record_identity_variant(
- &sandbox,
- account_id,
- Some(body_pubkey.as_str()),
- None,
- farm_d_tag,
- listing_d_tag,
- "body-only",
- "Body Only App Eggs",
- Some(json!({ "state": "exportable" })),
- false,
- );
-
- let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
- assert_eq!(list["result"]["count"], 1);
- let listing_row = &list["result"]["records"][0];
- assert_eq!(listing_row["record_id"], record_id);
- assert_eq!(listing_row["title"], "Body Only App Eggs");
- assert_eq!(listing_row["exportable"], false);
- assert_eq!(
- listing_row["reason"],
- "canonical hex pubkey required before export"
- );
- assert!(
- listing_row
- .get("actions")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- );
-
- let export_path = sandbox.root().join("body-only-app-eggs.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "listing",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!output.status.success());
- assert_eq!(export["result"], Value::Null);
- assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
- assert_eq!(
- export["errors"][0]["message"],
- "canonical hex pubkey required before export"
- );
- assert!(!export_path.exists());
-}
-
-#[test]
-fn listing_app_records_export_uses_record_owner_over_body_pubkey() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let owner_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- let body_pubkey = identity_public(92).public_key_hex;
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let record_id = seed_app_listing_record_identity_variant(
- &sandbox,
- account_id,
- Some(body_pubkey.as_str()),
- Some(owner_pubkey),
- farm_d_tag,
- listing_d_tag,
- "owner-wins",
- "Owner Wins App Eggs",
- None,
- true,
- );
-
- let export_path = sandbox.root().join("owner-wins-app-eggs.toml");
- let export_path_arg = export_path.to_string_lossy();
- let export = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert_eq!(export["operation_id"], "listing.app.export");
- assert_eq!(export["result"]["state"], "exported");
- assert_eq!(export["result"]["seller_pubkey"], owner_pubkey);
- let exported_contents = fs::read_to_string(&export_path).expect("exported listing draft");
- assert!(exported_contents.contains(format!("pubkey = \"{owner_pubkey}\"").as_str()));
- assert!(!exported_contents.contains(body_pubkey.as_str()));
-}
-
-#[test]
-fn order_app_records_list_export_get_and_submit_supported_app_order() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey");
- let seller_pubkey = identity_public(73).public_key_hex;
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_d_tag}");
- let listing_event_id = seed_orderable_listing(&sandbox, listing_addr.as_str());
- let order_id = "018f47a8-7b2c-7000-8000-000000000011";
- let record_id = seed_app_order_record(
- &sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- assert_eq!(app_list["operation_id"], "trade.app.list");
- assert_eq!(app_list["result"]["state"], "ready");
- assert_eq!(app_list["result"]["count"], 1);
- assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
- assert_eq!(app_list["result"]["records"][0]["trade_id"], order_id);
- assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], true);
- assert_eq!(app_list["result"]["records"][0]["exportable"], true);
- assert_no_removed_command_reference(&app_list, &["trade", "app", "list"]);
- assert_no_daemon_runtime_reference(&app_list, &["trade", "app", "list"]);
-
- let orders = sandbox.json_success(&["--format", "json", "trade", "list"]);
- assert_eq!(orders["operation_id"], "trade.list");
- assert_eq!(orders["result"]["state"], "ready");
- assert_eq!(orders["result"]["count"], 1);
- assert_eq!(orders["result"]["trades"][0]["id"], order_id);
- assert_eq!(orders["result"]["trades"][0]["ready_for_submit"], true);
- assert_eq!(
- orders["result"]["trades"][0]["listing_event_id"],
- listing_event_id
- );
- assert_eq!(
- orders["result"]["trades"][0]["buyer_account_id"],
- account_id
- );
- assert_eq!(
- orders["result"]["trades"][0]["file"],
- format!("shared-runtime-store/{record_id}")
- );
- assert_no_removed_command_reference(&orders, &["trade", "list"]);
- assert_no_daemon_runtime_reference(&orders, &["trade", "list"]);
-
- let get_by_record =
- sandbox.json_success(&["--format", "json", "trade", "get", record_id.as_str()]);
- assert_eq!(get_by_record["operation_id"], "trade.get");
- assert_eq!(get_by_record["result"]["state"], "ready");
- assert_eq!(get_by_record["result"]["trade_id"], order_id);
- assert_eq!(get_by_record["result"]["ready_for_submit"], true);
-
- let export_path = sandbox.root().join("app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert_eq!(dry_run["operation_id"], "trade.app.export");
- assert_eq!(dry_run["result"]["state"], "dry_run");
- assert_eq!(dry_run["result"]["valid"], true);
- assert!(!export_path.exists());
-
- let export = sandbox.json_success(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert_eq!(export["operation_id"], "trade.app.export");
- assert_eq!(export["result"]["state"], "exported");
- assert_eq!(export["result"]["trade_id"], order_id);
- assert!(export_path.exists());
- let exported_contents = fs::read_to_string(&export_path).expect("exported trade draft");
- assert!(exported_contents.contains("kind = \"order_draft_v1\""));
- assert!(exported_contents.contains(format!("order_id = \"{order_id}\"").as_str()));
- assert!(exported_contents.contains("source = \"resolved_account\""));
-
- let submit = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- record_id.as_str(),
- ]);
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["result"]["state"], "dry_run");
- assert_eq!(submit["result"]["source"], "SDK trade submit · local key");
- assert_eq!(submit["result"]["event_kind"], 3422);
- assert!(submit["result"]["target_transport_endpoints"].is_null());
- assert_eq!(
- submit["result"]["event_id"]
- .as_str()
- .expect("event id")
- .len(),
- 64
- );
-}
-
-#[test]
-fn order_app_records_treat_matching_signed_evidence_as_submitted() {
- let sandbox = RadrootsCliSandbox::new();
- let buyer = identity_secret(97);
- let buyer_public_file =
- write_public_identity_profile(&sandbox, "app-order-submitted-buyer", &buyer.to_public());
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- buyer_public_file.to_string_lossy().as_ref(),
- ]);
- let account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let buyer_secret_file = write_secret_identity_profile(&sandbox, "app-order-submitted", &buyer);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "attach-secret",
- account_id,
- buyer_secret_file.to_string_lossy().as_ref(),
- "--default",
- ]);
-
- let buyer_pubkey = buyer.public_key_hex();
- let seller_pubkey = identity_public(77).public_key_hex;
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_d_tag}");
- let listing_event_id = seed_orderable_listing(&sandbox, listing_addr.as_str());
- let order_id = "018f47a8-7b2c-7000-8000-000000000016";
- let record_id = seed_app_order_record(
- &sandbox,
- account_id,
- buyer_pubkey.as_str(),
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- );
- let signed_event = signed_app_order_request_event(
- &buyer,
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- seller_pubkey.as_str(),
- 2,
- );
- let signed_event_id = signed_event.id.to_hex();
- append_app_signed_order_request_record(
- &sandbox,
- account_id,
- listing_addr.as_str(),
- &signed_event,
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- let listed = &app_list["result"]["records"][0];
- assert_eq!(listed["record_id"], record_id);
- assert_eq!(listed["status"], "submitted");
- assert_eq!(listed["ready_for_submit"], false);
- assert_eq!(listed["exportable"], false);
- assert_eq!(
- listed["actions"].as_array().expect("actions"),
- &vec![json!(format!("radroots trade status get {order_id}"))]
- );
-
- let orders = sandbox.json_success(&["--format", "json", "trade", "list"]);
- assert_eq!(orders["result"]["state"], "ready");
- assert_eq!(orders["result"]["trades"][0]["state"], "submitted");
- assert_eq!(orders["result"]["trades"][0]["ready_for_submit"], false);
-
- let get_by_record =
- sandbox.json_success(&["--format", "json", "trade", "get", record_id.as_str()]);
- assert_eq!(get_by_record["result"]["state"], "submitted");
- assert_eq!(get_by_record["result"]["ready_for_submit"], false);
- assert_eq!(
- get_by_record["result"]["issues"][0]["code"],
- "app_order_already_submitted"
- );
- assert_eq!(
- get_by_record["result"]["issues"][0]["event_ids"][0],
- signed_event_id
- );
- assert_eq!(
- get_by_record["result"]["actions"]
- .as_array()
- .expect("actions"),
- &vec![json!(format!("radroots trade status get {order_id}"))]
- );
-
- let export_path = sandbox.root().join("submitted-app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (export_output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!export_output.status.success());
- assert_eq!(export["operation_id"], "trade.app.export");
- assert_eq!(export["errors"][0]["detail"]["state"], "already_submitted");
- assert_eq!(export["errors"][0]["detail"]["valid"], false);
- assert!(!export_path.exists());
-
- let submit = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- record_id.as_str(),
- ]);
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["result"]["state"], "submitted");
- assert_eq!(submit["result"]["deduplicated"], true);
- assert_eq!(submit["result"]["event_id"], signed_event_id);
- assert!(
- submit["result"]
- .get("actions")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- );
-}
-
-#[test]
-fn order_app_records_fail_closed_when_signed_evidence_conflicts() {
- let sandbox = RadrootsCliSandbox::new();
- let buyer = identity_secret(98);
- let buyer_public_file =
- write_public_identity_profile(&sandbox, "app-order-conflict-buyer", &buyer.to_public());
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- buyer_public_file.to_string_lossy().as_ref(),
- ]);
- let account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let buyer_secret_file = write_secret_identity_profile(&sandbox, "app-order-conflict", &buyer);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "attach-secret",
- account_id,
- buyer_secret_file.to_string_lossy().as_ref(),
- "--default",
- ]);
-
- let buyer_pubkey = buyer.public_key_hex();
- let seller_pubkey = identity_public(78).public_key_hex;
- let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
- let listing_event_id = seed_orderable_listing(&sandbox, listing_addr.as_str());
- let order_id = "018f47a8-7b2c-7000-8000-000000000017";
- let record_id = seed_app_order_record(
- &sandbox,
- account_id,
- buyer_pubkey.as_str(),
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- );
- let signed_event = signed_app_order_request_event(
- &buyer,
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- seller_pubkey.as_str(),
- 3,
- );
- append_app_signed_order_request_record(
- &sandbox,
- account_id,
- listing_addr.as_str(),
- &signed_event,
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- let listed = &app_list["result"]["records"][0];
- assert_eq!(listed["record_id"], record_id);
- assert_eq!(listed["status"], "conflict");
- assert_eq!(listed["ready_for_submit"], false);
- assert_eq!(listed["exportable"], false);
- assert!(
- listed["reason"]
- .as_str()
- .expect("conflict reason")
- .contains("conflicts")
- );
- assert!(
- !listed["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .any(|action| action
- .as_str()
- .expect("action")
- .contains("order app export"))
- );
-
- let export_path = sandbox.root().join("conflicting-signed-app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (export_output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!export_output.status.success());
- assert_eq!(export["operation_id"], "trade.app.export");
- assert_eq!(export["errors"][0]["detail"]["state"], "conflict");
- assert_eq!(
- export["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_signed_evidence_conflict"
- );
- assert!(!export_path.exists());
-
- let (submit_output, submit) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- record_id.as_str(),
- ]);
- assert!(!submit_output.status.success());
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["errors"][0]["detail"]["state"], "invalid");
- assert_eq!(
- submit["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_signed_evidence_conflict"
- );
-}
-
-#[test]
-fn order_app_records_fail_closed_when_not_current_or_supported() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey");
- let seller_pubkey = identity_public(74).public_key_hex;
- let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
- let listing_event_id = "2".repeat(64);
- let stale_order_id = "018f47a8-7b2c-7000-8000-000000000012";
- let stale_record_id = seed_app_order_record_variant(
- &sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey.as_str(),
- stale_order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- false,
- "supported",
- Vec::new(),
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- assert_eq!(
- app_list["result"]["records"][0]["record_id"],
- stale_record_id
- );
- assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
- assert_eq!(app_list["result"]["records"][0]["exportable"], false);
- assert!(
- app_list["result"]["records"][0]["reason"]
- .as_str()
- .expect("stale reason")
- .contains("not marked current")
- );
-
- let export_path = sandbox.root().join("stale-app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (output, stale_export) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- stale_record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!output.status.success());
- assert_eq!(stale_export["operation_id"], "trade.app.export");
- assert_eq!(stale_export["result"], Value::Null);
- assert_eq!(stale_export["errors"][0]["detail"]["state"], "stale");
- assert_eq!(stale_export["errors"][0]["detail"]["valid"], false);
- assert!(!export_path.exists());
-
- let (submit_output, submit) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- stale_record_id.as_str(),
- ]);
- assert!(!submit_output.status.success());
- assert_eq!(submit_output.status.code(), Some(3));
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["errors"][0]["code"], "operation_unavailable");
- assert_eq!(
- submit["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_stale"
- );
-}
-
-#[test]
-fn order_app_records_fail_closed_when_unsupported() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey");
- let seller_pubkey = identity_public(75).public_key_hex;
- let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
- let listing_event_id = "3".repeat(64);
- let order_id = "018f47a8-7b2c-7000-8000-000000000013";
- let record_id = seed_app_order_record_variant(
- &sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- true,
- "unsupported",
- vec!["seller_pubkey_required"],
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
- assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
- assert_eq!(app_list["result"]["records"][0]["exportable"], false);
- assert!(
- app_list["result"]["records"][0]["reason"]
- .as_str()
- .expect("unsupported reason")
- .contains("not marked supported")
- );
-
- let export_path = sandbox.root().join("unsupported-app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (export_output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!export_output.status.success());
- assert_eq!(export["operation_id"], "trade.app.export");
- assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
- assert_eq!(
- export["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_unsupported"
- );
- assert!(!export_path.exists());
-
- let (submit_output, submit) =
- sandbox.json_output(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert!(!submit_output.status.success());
- assert_eq!(
- submit["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_unsupported"
- );
-}
-
-#[test]
-fn order_app_records_fail_closed_when_supported_record_is_malformed() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey");
- let seller_pubkey = identity_public(75).public_key_hex;
- let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
- let listing_event_id = "3".repeat(64);
- let order_id = "018f47a8-7b2c-7000-8000-000000000014";
- let record_id = seed_app_order_record_variant(
- &sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- true,
- "supported",
- vec!["unit_price_required"],
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
- assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
- assert_eq!(app_list["result"]["records"][0]["exportable"], false);
- assert!(
- app_list["result"]["records"][0]["reason"]
- .as_str()
- .expect("malformed reason")
- .contains("support_status.issues")
- );
-
- let export_path = sandbox.root().join("malformed-app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (export_output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!export_output.status.success());
- assert_eq!(export["operation_id"], "trade.app.export");
- assert_eq!(export["errors"][0]["detail"]["state"], "invalid");
- assert_eq!(
- export["errors"][0]["detail"]["issues"][0]["code"],
- "invalid_app_order_record"
- );
- assert!(!export_path.exists());
-
- let (submit_output, submit) =
- sandbox.json_output(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert!(!submit_output.status.success());
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(
- submit["errors"][0]["detail"]["issues"][0]["code"],
- "invalid_app_order_record"
- );
-}
-
-#[test]
-fn order_app_records_fail_closed_when_order_id_conflicts() {
- let sandbox = RadrootsCliSandbox::new();
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey");
- let seller_pubkey = identity_public(76).public_key_hex;
- let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
- let listing_event_id = "4".repeat(64);
- let order_id = "018f47a8-7b2c-7000-8000-000000000015";
- let first_record_id = seed_app_order_record(
- &sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- );
- let conflicting_record_id = format!("app:local_work:order_request:{order_id}:conflict");
- seed_app_order_record_variant_with_record_id(
- &sandbox,
- account_id,
- buyer_pubkey,
- seller_pubkey.as_str(),
- order_id,
- listing_addr.as_str(),
- listing_event_id.as_str(),
- conflicting_record_id.clone(),
- true,
- "supported",
- Vec::new(),
- );
-
- let app_list = sandbox.json_success(&["--format", "json", "trade", "app", "list"]);
- assert_eq!(app_list["result"]["count"], 1);
- assert_eq!(
- app_list["result"]["records"][0]["record_id"],
- conflicting_record_id
- );
- assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
- assert_eq!(app_list["result"]["records"][0]["exportable"], false);
- assert!(
- app_list["result"]["records"][0]["reason"]
- .as_str()
- .expect("conflict reason")
- .contains(first_record_id.as_str())
- );
-
- let export_path = sandbox.root().join("conflicting-app-order.toml");
- let export_path_arg = export_path.to_string_lossy();
- let (export_output, export) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "app",
- "export",
- conflicting_record_id.as_str(),
- "--output",
- export_path_arg.as_ref(),
- ]);
- assert!(!export_output.status.success());
- assert_eq!(export["operation_id"], "trade.app.export");
- assert_eq!(export["errors"][0]["detail"]["state"], "conflict");
- assert_eq!(
- export["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_conflict"
- );
- assert!(!export_path.exists());
-
- let (submit_output, submit) =
- sandbox.json_output(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert!(!submit_output.status.success());
- assert_eq!(
- submit["errors"][0]["detail"]["issues"][0]["code"],
- "app_order_conflict"
- );
-}
-
-#[test]
-fn farm_publish_uses_sdk_outbox_without_legacy_signed_event_records() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let relay_url = "ws://127.0.0.1:9";
- let runtime_store_records_before_publish = sandbox.runtime_store_records().len();
- configure_nostr_transport(&sandbox, relay_url);
-
- let (output, publish) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000101",
- "farm",
- "publish",
- ]);
-
- assert!(!output.status.success());
- assert_eq!(publish["operation_id"], "farm.publish");
- assert_eq!(publish["result"], serde_json::Value::Null);
- assert_eq!(publish["errors"][0]["code"], "network_unavailable");
- let detail = &publish["errors"][0]["detail"];
- assert_eq!(detail["source"], "SDK farm publish · configured signer");
- assert_eq!(detail["state"], "unavailable");
- assert_eq!(detail["profile"]["state"], "not_submitted");
- assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
- assert_eq!(detail["farm"]["state"], "unavailable");
- assert_eq!(detail["farm"]["target_transport_endpoints"][0], relay_url);
- assert_eq!(
- detail["farm"]["failed_transport_targets"][0]["endpoint_uri"],
- relay_url
- );
- assert_eq!(
- detail["farm"]["event_id"]
- .as_str()
- .expect("sdk farm event id")
- .len(),
- 64
- );
-
- let records = sandbox.runtime_store_records();
- assert_eq!(records.len(), runtime_store_records_before_publish);
- let signed_records = records
- .iter()
- .filter(|record| record.family == RuntimeStoreRecordFamily::SignedEvent)
- .collect::<Vec<_>>();
- assert!(signed_records.is_empty());
-}
-
-#[test]
-fn listing_publish_failure_uses_sdk_outbox_without_legacy_runtime_store_record() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag");
- let listing_file = create_listing_draft(&sandbox, "failed-outbox-eggs");
- make_listing_publishable(&listing_file, farm_d_tag);
- let relay_url = "ws://127.0.0.1:9";
- let runtime_store_records_before_publish = sandbox.runtime_store_records().len();
- configure_nostr_transport(&sandbox, relay_url);
-
- let (output, publish) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000106",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(publish["operation_id"], "listing.publish");
- assert_eq!(publish["errors"][0]["code"], "network_unavailable");
- assert_eq!(
- publish["errors"][0]["detail"]["source"],
- "SDK listing publish · configured signer"
- );
- assert_eq!(publish["errors"][0]["detail"]["state"], "unavailable");
- assert_eq!(
- publish["errors"][0]["detail"]["target_transport_endpoints"][0],
- relay_url
- );
- assert_eq!(
- publish["errors"][0]["detail"]["failed_transport_targets"][0]["endpoint_uri"],
- relay_url
- );
- assert_eq!(
- publish["errors"][0]["detail"]["actions"][0],
- "radroots sync push"
- );
- assert_eq!(
- publish["errors"][0]["detail"]["event_id"]
- .as_str()
- .expect("sdk event id")
- .len(),
- 64
- );
- assert_eq!(
- sandbox.runtime_store_records().len(),
- runtime_store_records_before_publish
- );
-}
-
-#[test]
-fn sync_push_sdk_outbox_failure_reports_network_unavailable() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Sync SDK Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- let farm_d_tag = farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag");
- let listing_file = create_listing_draft(&sandbox, "sync-sdk-push-eggs");
- make_listing_publishable(&listing_file, farm_d_tag);
- let relay = "ws://127.0.0.1:9";
- configure_nostr_transport(&sandbox, relay);
- let publish = sandbox.json_success(&[
- "--format",
- "json",
- "--offline",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000105",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert_eq!(publish["operation_id"], "listing.publish");
- assert_eq!(publish["result"]["state"], "queued");
- let status = sandbox.json_success(&["--format", "json", "sync", "status", "get"]);
- assert_eq!(
- status["result"]["source"],
- "SDK canonical event store and outbox"
- );
- assert_eq!(
- status["result"]["replica_store"],
- "derived_projection_not_checked"
- );
- assert_eq!(status["result"]["queue"]["pending_count"], 1);
- assert_eq!(status["result"]["queue"]["ready_signed_count"], 1);
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "sync",
- "push",
- ]);
-
- assert!(!output.status.success(), "{value}");
- assert_eq!(value["operation_id"], "sync.push");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "network_unavailable", "{value}");
- assert_eq!(value["errors"][0]["detail"]["state"], "unavailable");
- assert_eq!(value["errors"][0]["detail"]["source"], "SDK outbox push");
- assert_eq!(
- value["errors"][0]["detail"]["replica_store"],
- "derived_projection_not_checked"
- );
- assert_eq!(
- value["errors"][0]["detail"]["target_transport_endpoints"][0],
- relay
- );
- assert_eq!(
- value["errors"][0]["detail"]["failed_transport_targets"][0]["endpoint_uri"],
- relay
- );
- assert_eq!(value["errors"][0]["detail"]["publishable_count"], 1);
- assert_eq!(value["errors"][0]["detail"]["published_count"], 0);
- assert_eq!(value["errors"][0]["detail"]["failed_count"], 1);
- assert_eq!(
- value["errors"][0]["detail"]["reason"],
- "SDK outbox push did not reach accepted quorum for any ready event"
- );
- assert_eq!(
- value["errors"][0]["detail"]["actions"][0],
- "radroots sync push"
- );
- assert_no_removed_command_reference(&value, &["sync", "push"]);
- assert_no_daemon_runtime_reference(&value, &["sync", "push"]);
-}
-
-#[test]
-fn sync_push_ignores_derived_projection_pending_queue_for_sdk_canonical_push() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&["--format", "json", "store", "init"]);
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- let selected_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
- .as_str()
- .expect("selected public key");
- seed_derived_projection_sync_farm(
- &sandbox,
- DERIVED_PROJECTION_SYNC_PUSH_FARM_D_TAG,
- selected_pubkey,
- );
- let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica");
- let derived_projection_batch = radroots_replica_pending_publish_batch(&executor)
- .expect("derived projection pending batch");
- assert!(derived_projection_batch.pending_count > 0);
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "sync",
- "push",
- ]);
-
- assert_eq!(value["operation_id"], "sync.push");
- assert_eq!(value["result"]["state"], "ready");
- assert_eq!(value["result"]["source"], "SDK outbox push");
- assert_eq!(
- value["result"]["replica_store"],
- "derived_projection_not_checked"
- );
- assert_eq!(value["result"]["queue"]["pending_count"], 0);
- assert_eq!(value["result"]["queue"]["total_count"], 0);
- assert_eq!(value["result"]["publishable_count"], 0);
- assert_eq!(value["result"]["published_count"], 0);
- assert_eq!(value["result"]["failed_count"], 0);
- assert_eq!(
- value["result"]["reason"],
- "SDK outbox had no ready signed events to push"
- );
- assert_no_removed_command_reference(&value, &["sync", "push"]);
- assert_no_daemon_runtime_reference(&value, &["sync", "push"]);
-}
-
-#[test]
-fn buyer_market_sync_basket_dry_runs_preflight_without_mutating_local_state() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
-
- let market = sandbox.json_success(&["--format", "json", "--dry-run", "market", "refresh"]);
- assert_eq!(market["operation_id"], "market.refresh");
- assert_eq!(market["dry_run"], true);
- assert_eq!(market["result"]["state"], "unconfigured");
- assert_eq!(market["result"]["replica_store"], "missing");
-
- let (sync_pull_output, sync_pull) =
- sandbox.json_output(&["--format", "json", "--dry-run", "sync", "pull"]);
- assert!(!sync_pull_output.status.success());
- assert_eq!(sync_pull["operation_id"], "sync.pull");
- assert_eq!(sync_pull["dry_run"], true);
- assert_eq!(sync_pull["errors"][0]["code"], "operation_unavailable");
- assert_eq!(sync_pull["errors"][0]["detail"]["state"], "unconfigured");
- assert_eq!(sync_pull["errors"][0]["detail"]["replica_store"], "missing");
-
- let sync_push = sandbox.json_success(&["--format", "json", "--dry-run", "sync", "push"]);
- assert_eq!(sync_push["operation_id"], "sync.push");
- assert_eq!(sync_push["dry_run"], true);
- assert_eq!(sync_push["result"]["state"], "ready");
- assert_eq!(sync_push["result"]["source"], "SDK outbox push");
- assert_eq!(
- sync_push["result"]["replica_store"],
- "derived_projection_not_checked"
- );
- assert_eq!(sync_push["result"]["queue"]["pending_count"], 0);
- assert_eq!(sync_push["result"]["queue"]["total_count"], 0);
- assert_eq!(sync_push["result"]["publishable_count"], 0);
- assert_eq!(sync_push["result"]["published_count"], 0);
-
- sandbox.json_success(&["--format", "json", "store", "init"]);
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
- let relay_refresh =
- sandbox.json_success(&["--format", "json", "--dry-run", "market", "refresh"]);
- assert_eq!(relay_refresh["operation_id"], "market.refresh");
- assert_eq!(relay_refresh["dry_run"], true);
- assert_eq!(relay_refresh["result"]["state"], "ready");
- assert_eq!(
- relay_refresh["result"]["target_transport_endpoints"][0],
- "ws://127.0.0.1:9"
- );
- assert_eq!(relay_refresh["result"]["fetched_count"], 0);
- assert_eq!(relay_refresh["result"]["ingested_count"], 0);
-
- let sync_push_ready = sandbox.json_success(&["--format", "json", "--dry-run", "sync", "push"]);
- assert_eq!(sync_push_ready["operation_id"], "sync.push");
- assert_eq!(sync_push_ready["dry_run"], true);
- assert_eq!(sync_push_ready["result"]["state"], "ready");
- assert_eq!(sync_push_ready["result"]["source"], "SDK outbox push");
- assert_eq!(
- sync_push_ready["result"]["replica_store"],
- "derived_projection_not_checked"
- );
- assert_eq!(
- sync_push_ready["result"]["target_transport_endpoints"][0],
- "ws://127.0.0.1:9"
- );
- assert_eq!(sync_push_ready["result"]["publishable_count"], 0);
- assert_eq!(sync_push_ready["result"]["published_count"], 0);
-
- let empty_search =
- sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
- assert_eq!(empty_search["operation_id"], "market.product.search");
- assert_eq!(empty_search["result"]["state"], "empty");
-
- let create_dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "basket",
- "create",
- "basket_probe",
- ]);
- let basket_file = create_dry_run["result"]["file"]
- .as_str()
- .expect("basket file");
- assert_eq!(create_dry_run["operation_id"], "basket.create");
- assert_eq!(create_dry_run["result"]["state"], "dry_run");
- assert!(!Path::new(basket_file).exists());
-
- sandbox.json_success(&["--format", "json", "basket", "create", "basket_probe"]);
- let (collision_output, collision) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "basket",
- "create",
- "basket_probe",
- ]);
- assert!(!collision_output.status.success());
- assert_eq!(collision["operation_id"], "basket.create");
- assert_eq!(collision["errors"][0]["code"], "invalid_input");
-
- let before_add = sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
- let add = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "basket",
- "item",
- "add",
- "basket_probe",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- assert_eq!(add["operation_id"], "basket.item.add");
- assert_eq!(add["result"]["state"], "dry_run");
- let after_add = sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
- assert_eq!(after_add["result"], before_add["result"]);
-
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "basket_probe",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "basket",
- "quote",
- "create",
- "basket_probe",
- ]);
- assert_eq!(quote["operation_id"], "basket.quote.create");
- assert_eq!(quote["result"]["state"], "unconfigured");
- assert_eq!(quote["result"]["ready_for_quote"], false);
- assert_eq!(
- quote["result"]["issues"][0]["code"],
- "basket_item_listing_unresolved"
- );
-
- let basket_after_quote =
- sandbox.json_success(&["--format", "json", "basket", "get", "basket_probe"]);
- assert_eq!(basket_after_quote["result"]["quote"], Value::Null);
-}
-
-#[test]
-fn market_order_request_readiness_gates_buyer_intent_actions() {
- let sandbox = RadrootsCliSandbox::new();
- configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
-
- let search = sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
- assert_eq!(search["operation_id"], "market.product.search");
- let result = &search["result"]["results"][0];
- assert_eq!(result["protocol_valid"], true);
- assert_eq!(result["marketplace_eligible"], true);
- assert_eq!(result["checkout_enabled"], true);
- assert_eq!(result["primary_bin_verified"], true);
- assert!(result.get("reason_codes").is_none());
- assert!(
- search["result"]["actions"]
- .as_array()
- .expect("search actions")
- .iter()
- .any(|action| action == "radroots basket create")
- );
-
- let listing = sandbox.json_success(&[
- "--format",
- "json",
- "market",
- "listing",
- "get",
- "pasture-eggs",
- ]);
- assert_eq!(listing["operation_id"], "market.listing.get");
- assert_eq!(listing["result"]["protocol_valid"], true);
- assert_eq!(listing["result"]["marketplace_eligible"], true);
- assert_eq!(listing["result"]["checkout_enabled"], true);
- assert_eq!(listing["result"]["primary_bin_verified"], true);
- assert!(
- listing["result"]["actions"]
- .as_array()
- .expect("listing actions")
- .iter()
- .any(|action| action == "radroots basket create")
- );
-
- update_orderable_listing_available_amount(&sandbox, LISTING_ADDR, 0);
-
- let disabled_search =
- sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
- let disabled_result = &disabled_search["result"]["results"][0];
- assert_eq!(disabled_result["protocol_valid"], true);
- assert_eq!(disabled_result["marketplace_eligible"], true);
- assert_eq!(disabled_result["checkout_enabled"], false);
- assert_eq!(disabled_result["primary_bin_verified"], true);
- assert_eq!(
- disabled_result["reason_codes"][0],
- "listing_checkout_disabled"
- );
- assert_eq!(
- disabled_result["reason_codes"][1],
- "listing_inventory_unavailable"
- );
- assert!(
- disabled_search["result"]["actions"]
- .as_array()
- .expect("disabled search actions")
- .iter()
- .all(|action| action != "radroots basket create")
- );
-
- let disabled_listing = sandbox.json_success(&[
- "--format",
- "json",
- "market",
- "listing",
- "get",
- "pasture-eggs",
- ]);
- assert_eq!(disabled_listing["result"]["protocol_valid"], true);
- assert_eq!(disabled_listing["result"]["marketplace_eligible"], true);
- assert_eq!(disabled_listing["result"]["checkout_enabled"], false);
- assert_eq!(disabled_listing["result"]["primary_bin_verified"], true);
- assert_eq!(
- disabled_listing["result"]["reason_codes"][0],
- "listing_checkout_disabled"
- );
- assert!(
- disabled_listing["result"]
- .get("actions")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- );
-
- update_orderable_listing_available_amount(&sandbox, LISTING_ADDR, 5);
- update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, None);
-
- let no_bin_search =
- sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
- let no_bin_result = &no_bin_search["result"]["results"][0];
- assert_eq!(no_bin_result["primary_bin_id"], Value::Null);
- assert_eq!(no_bin_result["checkout_enabled"], false);
- assert_eq!(no_bin_result["primary_bin_verified"], false);
- assert_eq!(
- no_bin_result["reason_codes"][0],
- "listing_checkout_disabled"
- );
- assert_eq!(
- no_bin_result["reason_codes"][1],
- "listing_primary_bin_missing"
- );
- assert!(
- no_bin_search["result"]["actions"]
- .as_array()
- .expect("no-bin search actions")
- .iter()
- .all(|action| action != "radroots basket create")
- );
-
- let no_bin_listing = sandbox.json_success(&[
- "--format",
- "json",
- "market",
- "listing",
- "get",
- "pasture-eggs",
- ]);
- assert_eq!(no_bin_listing["result"]["primary_bin_id"], Value::Null);
- assert_eq!(no_bin_listing["result"]["checkout_enabled"], false);
- assert_eq!(no_bin_listing["result"]["primary_bin_verified"], false);
- assert_eq!(
- no_bin_listing["result"]["reason_codes"][1],
- "listing_primary_bin_missing"
- );
- assert!(
- no_bin_listing["result"]
- .get("actions")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- );
-
- update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("missing-bin"));
-
- let invalid_bin_search =
- sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
- let invalid_bin_result = &invalid_bin_search["result"]["results"][0];
- assert_eq!(invalid_bin_result["primary_bin_id"], "missing-bin");
- assert_eq!(invalid_bin_result["checkout_enabled"], false);
- assert_eq!(invalid_bin_result["primary_bin_verified"], false);
- assert_eq!(
- invalid_bin_result["reason_codes"][1],
- "listing_primary_bin_invalid"
- );
- assert!(
- invalid_bin_search["result"]["actions"]
- .as_array()
- .expect("invalid-bin search actions")
- .iter()
- .all(|action| action != "radroots basket create")
- );
-
- let invalid_bin_listing = sandbox.json_success(&[
- "--format",
- "json",
- "market",
- "listing",
- "get",
- "pasture-eggs",
- ]);
- assert_eq!(
- invalid_bin_listing["result"]["primary_bin_id"],
- "missing-bin"
- );
- assert_eq!(invalid_bin_listing["result"]["checkout_enabled"], false);
- assert_eq!(invalid_bin_listing["result"]["primary_bin_verified"], false);
- assert_eq!(
- invalid_bin_listing["result"]["reason_codes"][1],
- "listing_primary_bin_invalid"
- );
- assert!(
- invalid_bin_listing["result"]
- .get("actions")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- );
-
- update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("bin-1"));
-
- let restored_listing = sandbox.json_success(&[
- "--format",
- "json",
- "market",
- "listing",
- "get",
- "pasture-eggs",
- ]);
- assert_eq!(restored_listing["result"]["primary_bin_id"], "bin-1");
- assert_eq!(restored_listing["result"]["checkout_enabled"], true);
- assert_eq!(restored_listing["result"]["primary_bin_verified"], true);
- assert!(
- restored_listing["result"]
- .get("reason_codes")
- .is_none_or(Value::is_null)
- );
- assert!(
- restored_listing["result"]["actions"]
- .as_array()
- .expect("restored listing actions")
- .iter()
- .any(|action| action == "radroots basket create")
- );
-}
-
-#[test]
-fn required_approval_token_rejects_absent_empty_and_whitespace_values() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(61);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "approval-import", &public_identity);
- let public_identity_path = public_identity_file.to_string_lossy();
-
- assert_required_approval_token_rejected(
- &sandbox,
- "account.import",
- &["account", "import", public_identity_path.as_ref()],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "account.remove",
- &["account", "remove", "acct_missing"],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "farm.rebind",
- &["farm", "rebind", "acct_missing"],
- );
- assert_required_approval_token_rejected(&sandbox, "farm.publish", &["farm", "publish"]);
- assert_required_approval_token_rejected(
- &sandbox,
- "listing.publish",
- &["listing", "publish", "missing-listing.toml"],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "listing.update",
- &["listing", "update", "missing-listing.toml"],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "listing.archive",
- &["listing", "archive", "missing-listing.toml"],
- );
- assert_required_approval_token_rejected(&sandbox, "sync.push", &["sync", "push"]);
- assert_required_approval_token_rejected(&sandbox, "trade.submit", &["trade", "submit"]);
- assert_required_approval_token_rejected(
- &sandbox,
- "trade.rebind",
- &["trade", "rebind", "ord_missing", "acct_missing"],
- );
- assert_required_approval_token_rejected(&sandbox, "trade.accept", &["trade", "accept"]);
- assert_required_approval_token_rejected(
- &sandbox,
- "trade.decline",
- &["trade", "decline", "--reason", "out_of_stock"],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "trade.cancel",
- &["trade", "cancel", "--reason", "changed plans"],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "trade.revision.accept",
- &[
- "trade",
- "revision",
- "accept",
- "ord_pending",
- "--revision-id",
- "rev_pending",
- ],
- );
- assert_required_approval_token_rejected(
- &sandbox,
- "trade.revision.decline",
- &[
- "trade",
- "revision",
- "decline",
- "ord_pending",
- "--revision-id",
- "rev_pending",
- "--reason",
- "keep original order",
- ],
- );
-}
-
-fn assert_required_approval_token_rejected(
- sandbox: &RadrootsCliSandbox,
- operation_id: &str,
- command_args: &[&str],
-) {
- for token in [None, Some(""), Some(" \t ")] {
- let mut args = vec!["--format", "json"];
- if let Some(token) = token {
- args.push("--approval-token");
- args.push(token);
- }
- args.extend_from_slice(command_args);
-
- let (output, value) = sandbox.json_output(&args);
-
- assert_eq!(output.status.code(), Some(6), "`{args:?}` should fail");
- assert_eq!(value["operation_id"], operation_id);
- assert_eq!(value["errors"][0]["code"], "approval_required");
- assert_eq!(value["errors"][0]["exit_code"], 6);
- assert_no_removed_command_reference(&value, &args);
- }
-}
-
-#[test]
-fn order_submit_missing_order_returns_not_found_while_read_view_stays_successful() {
- let sandbox = RadrootsCliSandbox::new();
-
- let get = sandbox.json_success(&[
- "--format",
- "json",
- "trade",
- "get",
- "ord_missing_submit_target",
- ]);
- assert_eq!(get["operation_id"], "trade.get");
- assert_eq!(get["result"]["state"], "missing");
- assert_eq!(get["errors"].as_array().expect("errors").len(), 0);
-
- let (output, submit) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "trade",
- "submit",
- "ord_missing_submit_target",
- ]);
-
- assert_eq!(output.status.code(), Some(4));
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["errors"][0]["code"], "not_found");
- assert_eq!(submit["errors"][0]["exit_code"], 4);
- assert_eq!(submit["errors"][0]["detail"]["class"], "resource");
- assert_no_removed_command_reference(&submit, &["trade", "submit"]);
- assert_no_daemon_runtime_reference(&submit, &["trade", "submit"]);
-}
-
-fn create_ready_order(sandbox: &RadrootsCliSandbox, basket_id: &str) -> String {
- sandbox.json_success(&["--format", "json", "account", "create"]);
- configure_nostr_transport(sandbox, ORDERABLE_LISTING_RELAY);
- seed_orderable_listing(sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(sandbox, &[ORDERABLE_LISTING_RELAY]);
- sandbox.json_success(&["--format", "json", "basket", "create", basket_id]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- basket_id,
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&["--format", "json", "basket", "quote", "create", basket_id]);
- sandbox.write_app_config("[transport]\nprofile = \"local_only\"\n");
- quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id")
- .to_owned()
-}
-
-fn rewrite_order_bin(sandbox: &RadrootsCliSandbox, order_id: &str, bin_id: &str) {
- let path = sandbox
- .root()
- .join("data/apps/cli/orders/drafts")
- .join(format!("{order_id}.toml"));
- let contents = fs::read_to_string(&path).expect("read trade draft");
- let updated = contents.replace(
- "bin_id = \"bin-1\"",
- format!("bin_id = \"{bin_id}\"").as_str(),
- );
- assert_ne!(updated, contents);
- fs::write(path, updated).expect("rewrite trade draft bin");
-}
-
-fn rewrite_order_buyer_actor_pubkey(sandbox: &RadrootsCliSandbox, order_id: &str, pubkey: &str) {
- let path = sandbox
- .root()
- .join("data/apps/cli/orders/drafts")
- .join(format!("{order_id}.toml"));
- let contents = fs::read_to_string(&path).expect("read trade draft");
- let mut in_buyer_actor = false;
- let mut replaced = false;
- let updated = contents
- .lines()
- .map(|line| {
- let trimmed = line.trim_start();
- if trimmed.starts_with('[') {
- in_buyer_actor = trimmed == "[buyer_actor]";
- }
- if in_buyer_actor && trimmed.starts_with("pubkey =") {
- replaced = true;
- format!("{}pubkey = \"{}\"", line_indent(line), pubkey)
- } else {
- line.to_owned()
- }
- })
- .collect::<Vec<_>>()
- .join("\n");
- assert!(replaced, "buyer_actor pubkey field");
- fs::write(path, format!("{updated}\n")).expect("rewrite trade draft buyer actor");
-}
-
-fn line_indent(line: &str) -> &str {
- let trimmed = line.trim_start();
- &line[..line.len() - trimmed.len()]
-}
-
-fn signed_order_request_event_for_quote(
- buyer: &radroots_identity::RadrootsIdentity,
- order_id: &str,
- listing_event_id: &str,
- economics: RadrootsOrderEconomics,
-) -> RadrootsNostrEvent {
- let buyer_pubkey = buyer.public_key_hex();
- let seller_pubkey = "1".repeat(64);
- let payload = RadrootsOrderRequest {
- order_id: test_order_id(order_id),
- listing_addr: test_listing_addr(LISTING_ADDR),
- buyer_pubkey: test_pubkey(buyer_pubkey.as_str()),
- seller_pubkey: test_pubkey(seller_pubkey.as_str()),
- items: vec![RadrootsOrderItem {
- bin_id: test_inventory_bin_id("bin-1"),
- bin_count: 2,
- }],
- economics,
- };
- let parts = order_request_event_build(
- &RadrootsEventPtr {
- id: listing_event_id.to_owned(),
- relays: None,
- },
- &payload,
- )
- .expect("order request parts");
- radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("nostr event builder")
- .sign_with_keys(buyer.keys())
- .expect("signed order request")
-}
-
-#[test]
-fn buyer_target_flow_acceptance_uses_target_operations() {
- let sandbox = RadrootsCliSandbox::new();
-
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- assert_eq!(account["operation_id"], "account.create");
- assert_eq!(account["result"]["account"]["signer"], "local");
- assert_eq!(account["result"]["account"]["custody"], "secret_backed");
- assert_eq!(account["result"]["account"]["write_capable"], true);
- assert_no_removed_command_reference(&account, &["account", "create"]);
-
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- assert_eq!(signer["operation_id"], "signer.status.get");
- assert_eq!(signer["result"]["mode"], "local");
- assert_eq!(signer["result"]["state"], "ready");
- assert_eq!(signer["result"]["signer_account_id"], account_id);
- assert_no_removed_command_reference(&signer, &["signer", "status", "get"]);
-
- configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
- let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
-
- let search = sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
- assert_eq!(search["operation_id"], "market.product.search");
- assert_eq!(search["errors"].as_array().expect("errors").len(), 0);
- assert_no_removed_command_reference(&search, &["market", "product", "search"]);
-
- let create = sandbox.json_success(&["--format", "json", "basket", "create", "basket_flow"]);
- assert_eq!(create["operation_id"], "basket.create");
- assert_eq!(create["result"]["basket_id"], "basket_flow");
- assert_no_removed_command_reference(&create, &["basket", "create"]);
-
- let add = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "basket_flow",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- assert_eq!(add["operation_id"], "basket.item.add");
- assert_eq!(add["result"]["ready_for_quote"], true);
- assert_no_removed_command_reference(&add, &["basket", "item", "add"]);
-
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "basket_flow",
- ]);
- assert_eq!(quote["operation_id"], "basket.quote.create");
- assert_eq!(quote["result"]["state"], "quoted");
- assert_no_removed_command_reference("e, &["basket", "quote", "create"]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- let quote_economics = "e["result"]["quote"]["economics"];
- let order_file = quote["result"]["trade"]["file"]
- .as_str()
- .expect("order file");
- assert_eq!(quote["result"]["quote"]["ready_for_submit"], true);
- assert_eq!(quote["result"]["quote"]["quote_version"], 1);
- assert_eq!(
- quote["result"]["quote"]["quote_id"],
- quote_economics["quote_id"]
- );
- assert_eq!(quote_economics["quote_version"], 1);
- assert_eq!(quote_economics["pricing_basis"], "listing_event");
- assert_eq!(quote_economics["currency"], "USD");
- assert_eq!(quote_economics["items"][0]["bin_id"], "bin-1");
- assert_eq!(quote_economics["items"][0]["bin_count"], 2);
- assert_eq!(quote_economics["discounts"], Value::Array(Vec::new()));
- assert_eq!(quote_economics["adjustments"], Value::Array(Vec::new()));
- assert_eq!(
- quote["result"]["trade"]["economics"],
- quote_economics.clone()
- );
- let order_draft = fs::read_to_string(order_file).expect("read trade draft");
- assert!(order_draft.contains("[buyer_actor]"));
- assert!(order_draft.contains("source = \"resolved_account\""));
- assert!(order_draft.contains("[order.economics]"));
- assert!(order_draft.contains("pricing_basis = \"listing_event\""));
- assert_eq!(quote["result"]["trade"]["buyer_account_id"], account_id);
- assert_eq!(
- quote["result"]["trade"]["buyer_actor_source"],
- "resolved_account"
- );
- assert_eq!(
- quote["result"]["trade"]["listing_event_id"],
- listing_event_id
- );
-
- let orders = sandbox.json_success(&["--format", "json", "trade", "list"]);
- assert_eq!(orders["operation_id"], "trade.list");
- assert_eq!(orders["result"]["state"], "ready");
- assert_eq!(orders["result"]["count"], 1);
- assert_eq!(orders["result"]["trades"][0]["id"], order_id);
- assert_eq!(orders["result"]["trades"][0]["ready_for_submit"], true);
- assert_eq!(
- orders["result"]["trades"][0]["listing_event_id"],
- listing_event_id
- );
- assert_eq!(
- orders["result"]["trades"][0]["buyer_account_id"],
- account_id
- );
- assert_eq!(
- orders["result"]["trades"][0]["buyer_actor_source"],
- "resolved_account"
- );
- assert_eq!(
- orders["result"]["trades"][0]["economics"],
- quote_economics.clone()
- );
- assert_eq!(orders["result"]["trades"][0]["issues"], Value::Null);
- assert_no_removed_command_reference(&orders, &["trade", "list"]);
- assert_no_daemon_runtime_reference(&orders, &["trade", "list"]);
-
- let submit =
- sandbox.json_success(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["dry_run"], true);
- assert_eq!(submit["result"]["state"], "dry_run");
- assert_eq!(submit["result"]["source"], "SDK trade submit · local key");
- assert_eq!(submit["result"]["event_kind"], 3422);
- assert_eq!(
- submit["result"]["target_transport_endpoints"][0],
- ORDERABLE_LISTING_RELAY
- );
- assert_eq!(
- submit["result"]["event_id"]
- .as_str()
- .expect("event id")
- .len(),
- 64
- );
- assert_no_removed_command_reference(&submit, &["trade", "submit", "--dry-run"]);
- assert_no_daemon_runtime_reference(&submit, &["trade", "submit", "--dry-run"]);
-
- sandbox.write_app_config("[transport]\nprofile = \"local_only\"\n");
- let (output, unavailable_submit) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "trade",
- "submit",
- order_id,
- ]);
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2), "{unavailable_submit}");
- assert_eq!(unavailable_submit["operation_id"], "trade.submit");
- assert_eq!(unavailable_submit["result"], Value::Null);
- assert_eq!(
- unavailable_submit["errors"][0]["code"],
- "empty_transport_targets"
- );
- assert_eq!(
- unavailable_submit["errors"][0]["detail"]["class"],
- "configuration"
- );
- assert_eq!(
- unavailable_submit["errors"][0]["detail"]["detail"]["operation"],
- "publish transport profile"
- );
- assert!(
- unavailable_submit["errors"][0]["message"]
- .as_str()
- .expect("message")
- .contains("empty transport targets")
- );
- assert_no_removed_command_reference(&unavailable_submit, &["trade", "submit"]);
- assert_no_daemon_runtime_reference(&unavailable_submit, &["trade", "submit"]);
-
- let order_after_submit = sandbox.json_success(&["--format", "json", "trade", "get", order_id]);
- assert_eq!(order_after_submit["operation_id"], "trade.get");
- assert_eq!(order_after_submit["result"]["state"], "ready");
- assert_eq!(
- order_after_submit["result"]["economics"],
- quote_economics.clone()
- );
- assert_eq!(order_after_submit["result"]["job"], Value::Null);
- assert_eq!(order_after_submit["result"]["workflow"], Value::Null);
- assert_no_daemon_runtime_reference(&order_after_submit, &["trade", "get"]);
-
- let (watch_output, watch) =
- sandbox.json_output(&["--format", "json", "trade", "event", "watch", order_id]);
- assert!(!watch_output.status.success());
- assert_eq!(watch_output.status.code(), Some(3));
- assert_eq!(watch["operation_id"], "trade.event.watch");
- assert_eq!(watch["result"], Value::Null);
- assert_eq!(watch["errors"][0]["code"], "not_implemented");
- assert_eq!(watch["errors"][0]["detail"]["state"], "not_implemented");
- assert_eq!(watch["errors"][0]["detail"]["trade_id"], order_id);
- assert_eq!(
- watch["next_actions"][0]["command"],
- format!("radroots trade status get {order_id}")
- );
- assert_no_daemon_runtime_reference(&watch, &["trade", "event", "watch"]);
- assert!(
- !serde_json::to_string(&watch)
- .expect("watch json")
- .contains("local trade drafts")
- );
-}
-
-#[test]
-fn order_get_and_list_report_missing_bound_buyer_account() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "missing_buyer_account");
-
- let ready = sandbox.json_success(&["--format", "json", "trade", "get", order_id.as_str()]);
- let account_id = ready["result"]["buyer_account_id"]
- .as_str()
- .expect("buyer account id");
- assert_eq!(ready["result"]["state"], "ready");
- assert_eq!(ready["result"]["buyer_account_id"], account_id);
- assert_eq!(ready["result"]["buyer_custody"], "secret_backed");
- assert_eq!(ready["result"]["buyer_write_capable"], true);
-
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "remove",
- account_id,
- ]);
-
- let missing = sandbox.json_success(&["--format", "json", "trade", "get", order_id.as_str()]);
- assert_eq!(missing["operation_id"], "trade.get");
- assert_eq!(missing["result"]["state"], "draft");
- assert_eq!(missing["result"]["ready_for_submit"], false);
- assert_eq!(missing["result"]["buyer_account_id"], account_id);
- assert_eq!(missing["result"]["buyer_custody"], Value::Null);
- assert!(
- missing["result"]["issues"]
- .as_array()
- .expect("issues")
- .iter()
- .any(|issue| issue["code"] == "account_unresolved")
- );
- assert!(
- missing["result"]["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .any(|action| action == "radroots account import <path>")
- );
- assert!(
- missing["result"]["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .any(|action| action
- == &Value::String(format!("radroots trade rebind {order_id} <selector>")))
- );
-
- let list = sandbox.json_success(&["--format", "json", "trade", "list"]);
- assert_eq!(list["result"]["state"], "degraded");
- assert_eq!(list["result"]["trades"][0]["ready_for_submit"], false);
- assert!(
- list["result"]["trades"][0]["issues"]
- .as_array()
- .expect("issues")
- .iter()
- .any(|issue| issue["code"] == "account_unresolved")
- );
-
- let (submit_output, submit) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- order_id.as_str(),
- ]);
- assert!(!submit_output.status.success());
- assert_eq!(submit_output.status.code(), Some(5));
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["errors"][0]["code"], "account_unresolved");
- assert_eq!(submit["errors"][0]["detail"]["trade_id"], order_id);
-}
-
-#[test]
-fn order_get_marks_watch_only_bound_buyer_unready() {
- let sandbox = RadrootsCliSandbox::new();
- let public_identity = identity_public(92);
- let public_identity_file =
- write_public_identity_profile(&sandbox, "order-watch-only-buyer", &public_identity);
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- public_identity_file.to_string_lossy().as_ref(),
- ]);
- let account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("watch account id");
- assert_eq!(imported["result"]["account"]["custody"], "watch_only");
-
- configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
- sandbox.json_success(&["--format", "json", "basket", "create", "watch_buyer"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "watch_buyer",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "watch_buyer",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
-
- let get = sandbox.json_success(&["--format", "json", "trade", "get", order_id]);
- assert_eq!(get["result"]["state"], "draft");
- assert_eq!(get["result"]["ready_for_submit"], false);
- assert_eq!(get["result"]["buyer_account_id"], account_id);
- assert_eq!(get["result"]["buyer_custody"], "watch_only");
- assert_eq!(get["result"]["buyer_write_capable"], false);
- assert!(
- get["result"]["issues"]
- .as_array()
- .expect("issues")
- .iter()
- .any(|issue| issue["code"] == "account_watch_only")
- );
- assert!(
- get["result"]["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .any(|action| action
- == &Value::String(format!(
- "radroots account attach-secret {account_id} <path>"
- )))
- );
-
- let (submit_output, submit) =
- sandbox.json_output(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert!(!submit_output.status.success());
- assert_eq!(submit_output.status.code(), Some(7));
- assert_eq!(submit["operation_id"], "trade.submit");
- assert_eq!(submit["errors"][0]["code"], "account_watch_only");
- assert_eq!(
- submit["errors"][0]["detail"]["order_buyer_account_id"],
- account_id
- );
-}
-
-#[test]
-fn order_get_marks_bound_buyer_pubkey_mismatch_unready() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "mismatched_buyer_actor");
- let other_pubkey = identity_public(93).public_key_hex;
- rewrite_order_buyer_actor_pubkey(&sandbox, order_id.as_str(), other_pubkey.as_str());
-
- let get = sandbox.json_success(&["--format", "json", "trade", "get", order_id.as_str()]);
- assert_eq!(get["result"]["state"], "draft");
- assert_eq!(get["result"]["ready_for_submit"], false);
- assert_eq!(get["result"]["buyer_custody"], "secret_backed");
- assert_eq!(get["result"]["buyer_write_capable"], true);
- assert!(
- get["result"]["issues"]
- .as_array()
- .expect("issues")
- .iter()
- .any(|issue| issue["code"] == "account_mismatch")
- );
- assert!(
- get["result"]["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .any(|action| action
- == &Value::String(format!("radroots trade rebind {order_id} <selector>")))
- );
-}
-
-#[test]
-fn order_rebind_previews_and_writes_bound_buyer_actor_updates() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "order_rebind");
- let order_file = sandbox
- .root()
- .join("data/apps/cli/orders/drafts")
- .join(format!("{order_id}.toml"));
- let before = fs::read_to_string(&order_file).expect("order before rebind");
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
-
- let dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "rebind",
- order_id.as_str(),
- second_account_id,
- ]);
- assert_eq!(dry_run["operation_id"], "trade.rebind");
- assert_eq!(dry_run["result"]["state"], "dry_run");
- assert_eq!(dry_run["result"]["from_trade_id"], order_id);
- assert_eq!(dry_run["result"]["trade_id_changed"], true);
- assert_eq!(dry_run["result"]["buyer_pubkey_changed"], true);
- assert_eq!(dry_run["result"]["to_buyer_account_id"], second_account_id);
- assert_eq!(
- dry_run["result"]["existing_request_check"],
- "skipped_no_relays"
- );
- assert_eq!(
- fs::read_to_string(&order_file).expect("order after dry-run rebind"),
- before
- );
-
- let (unapproved_output, unapproved) = sandbox.json_output(&[
- "--format",
- "json",
- "trade",
- "rebind",
- order_id.as_str(),
- second_account_id,
- ]);
- assert!(!unapproved_output.status.success());
- assert_eq!(unapproved["operation_id"], "trade.rebind");
- assert_eq!(unapproved["errors"][0]["code"], "approval_required");
-
- let rebound = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "trade",
- "rebind",
- order_id.as_str(),
- second_account_id,
- ]);
- assert_eq!(rebound["operation_id"], "trade.rebind");
- assert_eq!(rebound["result"]["state"], "rebound");
- assert_eq!(rebound["result"]["from_trade_id"], order_id);
- assert_eq!(rebound["result"]["trade_id_changed"], true);
- let rebound_order_id = rebound["result"]["to_trade_id"]
- .as_str()
- .expect("rebound order id");
- assert_ne!(rebound_order_id, order_id);
- let rebound_file = rebound["result"]["file"].as_str().expect("rebound file");
- assert!(!order_file.exists());
- let after = fs::read_to_string(rebound_file).expect("order after rebind");
- assert!(after.contains("[buyer_actor]"));
- assert!(after.contains("source = \"order_rebind\""));
- assert!(after.contains(format!("order_id = \"{rebound_order_id}\"").as_str()));
- assert!(after.contains(format!("quote_id = \"quote_{rebound_order_id}\"").as_str()));
-
- let get = sandbox.json_success(&["--format", "json", "trade", "get", rebound_order_id]);
- assert_eq!(get["result"]["state"], "ready");
- assert_eq!(get["result"]["buyer_account_id"], second_account_id);
- assert_eq!(get["result"]["buyer_actor_source"], "order_rebind");
-
- let same = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "trade",
- "rebind",
- rebound_order_id,
- second_account_id,
- ]);
- assert_eq!(same["result"]["state"], "rebound");
- assert_eq!(same["result"]["trade_id_changed"], false);
- assert_eq!(same["result"]["to_trade_id"], rebound_order_id);
-}
-
-#[test]
-fn order_rebind_refuses_visible_published_request() {
- let sandbox = RadrootsCliSandbox::new();
- let buyer = identity_secret(94);
- let buyer_public = buyer.to_public();
- let buyer_public_file =
- write_public_identity_profile(&sandbox, "rebind-visible-buyer", &buyer_public);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- buyer_public_file.to_string_lossy().as_ref(),
- ]);
- let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
- sandbox.json_success(&["--format", "json", "basket", "create", "visible_rebind"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "visible_rebind",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "visible_rebind",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- let economics: RadrootsOrderEconomics =
- serde_json::from_value(quote["result"]["quote"]["economics"].clone())
- .expect("quote economics");
- let event = signed_order_request_event_for_quote(
- &buyer,
- order_id,
- listing_event_id.as_str(),
- economics,
- );
- let target = sandbox.json_success(&["--format", "json", "account", "create"]);
- let target_account_id = target["result"]["account"]["id"]
- .as_str()
- .expect("target account id");
- let relay = RelayFetchServer::with_events(vec![event]);
- configure_nostr_transport(&sandbox, relay.endpoint());
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "rebind",
- order_id,
- target_account_id,
- ]);
- relay.join();
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(10));
- assert_eq!(value["operation_id"], "trade.rebind");
- assert_eq!(value["errors"][0]["code"], "validation_failed");
- assert_eq!(
- value["errors"][0]["detail"]["existing_request_check"],
- "blocked_existing_request"
- );
- assert_eq!(
- value["errors"][0]["detail"]["existing_request_event_ids"]
- .as_array()
- .expect("existing request ids")
- .len(),
- 1
- );
-}
-
-#[test]
-fn order_status_and_event_list_use_draft_context_after_account_override_drift() {
- let sandbox = RadrootsCliSandbox::new();
- let buyer = identity_secret(95);
- let buyer_public_file =
- write_public_identity_profile(&sandbox, "status-draft-buyer", &buyer.to_public());
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- buyer_public_file.to_string_lossy().as_ref(),
- ]);
- let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
- sandbox.json_success(&["--format", "json", "basket", "create", "draft_status"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "draft_status",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "draft_status",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- let economics: RadrootsOrderEconomics =
- serde_json::from_value(quote["result"]["quote"]["economics"].clone())
- .expect("quote economics");
- let event = signed_order_request_event_for_quote(
- &buyer,
- order_id,
- listing_event_id.as_str(),
- economics,
- );
- let drift_account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let drift_account_id = drift_account["result"]["account"]["id"]
- .as_str()
- .expect("drift account id");
-
- let status = sandbox.json_success(&[
- "--format",
- "json",
- "--account-id",
- drift_account_id,
- "trade",
- "status",
- "get",
- order_id,
- ]);
-
- assert_eq!(status["operation_id"], "trade.status.get");
- assert_eq!(status["result"]["source"], "SDK local trade projection");
- assert_eq!(
- status["result"]["actor_context_source"],
- "sdk_local_projection"
- );
- assert_eq!(status["result"]["state"], "missing");
- assert_eq!(status["result"]["fetched_count"], 0);
- assert_eq!(status["result"]["decoded_count"], 0);
-
- let event_list_relay = RelayFetchServer::with_events(vec![event]);
- configure_nostr_transport(&sandbox, event_list_relay.endpoint());
- let events = sandbox.json_success(&[
- "--format",
- "json",
- "--account-id",
- drift_account_id,
- "trade",
- "event",
- "list",
- order_id,
- ]);
- event_list_relay.join();
-
- assert_eq!(events["operation_id"], "trade.event.list");
- assert_eq!(events["result"]["actor_context_source"], "order_draft");
- assert_eq!(events["result"]["seller_pubkey"], "1".repeat(64));
- assert_eq!(events["result"]["count"], 1);
- assert_eq!(events["result"]["trades"][0]["id"], order_id);
-}
-
-#[test]
-fn order_cancel_uses_bound_buyer_after_default_account_drift() {
- let sandbox = RadrootsCliSandbox::new();
- let buyer = identity_secret(96);
- let buyer_public_file =
- write_public_identity_profile(&sandbox, "cancel-bound-buyer", &buyer.to_public());
- let imported = sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "import",
- "--default",
- buyer_public_file.to_string_lossy().as_ref(),
- ]);
- let buyer_account_id = imported["result"]["account"]["id"]
- .as_str()
- .expect("buyer account id");
- let buyer_secret_file = write_secret_identity_profile(&sandbox, "cancel-bound-secret", &buyer);
- sandbox.json_success(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "account",
- "attach-secret",
- buyer_account_id,
- buyer_secret_file.to_string_lossy().as_ref(),
- "--default",
- ]);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- sandbox.json_success(&["--format", "json", "basket", "create", "bound_cancel"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "bound_cancel",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "bound_cancel",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- let drift_account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let drift_account_id = drift_account["result"]["account"]["id"]
- .as_str()
- .expect("drift account id");
- sandbox.json_success(&[
- "--format",
- "json",
- "account",
- "selection",
- "update",
- drift_account_id,
- ]);
- let (cancel_output, cancel) = sandbox.json_output(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "cancel",
- order_id,
- "--reason",
- "changed plans",
- ]);
-
- assert!(!cancel_output.status.success());
- assert_eq!(cancel["operation_id"], "trade.cancel");
- assert_eq!(cancel["result"], Value::Null);
- assert_eq!(cancel["errors"][0]["code"], "empty_transport_targets");
- assert_eq!(cancel["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&cancel["errors"][0]["message"], "empty transport targets");
-}
-
-#[test]
-fn buyer_side_order_writes_reject_conflicting_account_override_for_local_draft() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "buyer_write_drift");
- let drift_account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let drift_account_id = drift_account["result"]["account"]["id"]
- .as_str()
- .expect("drift account id");
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
-
- for (operation_id, command) in [(
- "trade.cancel",
- vec![
- "--format",
- "json",
- "--dry-run",
- "--account-id",
- drift_account_id,
- "trade",
- "cancel",
- order_id.as_str(),
- "--reason",
- "changed plans",
- ],
- )] {
- let (output, value) = sandbox.json_output(command.as_slice());
-
- assert!(!output.status.success(), "{operation_id} should fail");
- assert_eq!(output.status.code(), Some(5));
- assert_eq!(value["operation_id"], operation_id);
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "account_mismatch");
- assert_eq!(value["errors"][0]["detail"]["trade_id"], order_id);
- assert_eq!(
- value["errors"][0]["detail"]["attempted_buyer_account_id"],
- drift_account_id
- );
- }
-}
-
-#[test]
-fn order_submit_non_dry_run_uses_sdk_relay_validation_without_replica_freshness_gate() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "freshness_missing_db");
- fs::remove_file(sandbox.replica_store_path()).expect("remove replica db");
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000200",
- "trade",
- "submit",
- order_id.as_str(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2));
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["errors"][0]["code"], "invalid_relay_url");
- assert_eq!(value["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&value["errors"][0]["message"], "loopback IPv4 address");
-}
-
-#[test]
-fn order_submit_dry_run_does_not_require_local_replica_freshness() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "dry_freshness_missing_db");
- fs::remove_file(sandbox.replica_store_path()).expect("remove replica db");
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- order_id.as_str(),
- ]);
-
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["source"], "SDK trade submit · local key");
-}
-
-#[test]
-fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_state_gate() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "freshness_missing_listing");
- remove_orderable_listing(&sandbox, LISTING_ADDR);
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000201",
- "trade",
- "submit",
- order_id.as_str(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2));
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["errors"][0]["code"], "invalid_relay_url");
- assert_eq!(value["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&value["errors"][0]["message"], "loopback IPv4 address");
-}
-
-#[test]
-fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_event_gate() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "freshness_superseded_listing");
- let replacement_event_id = "3".repeat(64);
- replace_latest_listing_event_id(&sandbox, LISTING_ADDR, replacement_event_id.as_str());
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000202",
- "trade",
- "submit",
- order_id.as_str(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2));
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["errors"][0]["code"], "invalid_relay_url");
- assert_eq!(value["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&value["errors"][0]["message"], "loopback IPv4 address");
-}
-
-#[test]
-fn order_submit_non_dry_run_uses_sdk_relay_validation_without_quantity_gate() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
- sandbox.json_success(&["--format", "json", "basket", "create", "over_quantity"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "over_quantity",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "6",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "over_quantity",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000203",
- "trade",
- "submit",
- order_id,
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2));
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["errors"][0]["code"], "invalid_relay_url");
- assert_eq!(value["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&value["errors"][0]["message"], "loopback IPv4 address");
- assert_no_removed_command_reference(&value, &["trade", "submit"]);
- assert_no_daemon_runtime_reference(&value, &["trade", "submit"]);
-}
-
-#[test]
-fn order_submit_non_dry_run_uses_sdk_relay_validation_without_bin_gate() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "unknown_bin");
- rewrite_order_bin(&sandbox, order_id.as_str(), "unknown-bin");
- configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "01890f3c-7f2a-7b01-8000-000000000204",
- "trade",
- "submit",
- order_id.as_str(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(2));
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["errors"][0]["code"], "invalid_relay_url");
- assert_eq!(value["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&value["errors"][0]["message"], "loopback IPv4 address");
- assert_no_removed_command_reference(&value, &["trade", "submit"]);
- assert_no_daemon_runtime_reference(&value, &["trade", "submit"]);
-}
-
-#[test]
-fn basket_quote_rejects_missing_replica_before_order_write() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&["--format", "json", "basket", "create", "missing_replica"]);
- let add = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "missing_replica",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- assert_eq!(add["result"]["ready_for_quote"], false);
- assert_eq!(
- add["result"]["issues"][0]["code"],
- "basket_market_replica_missing"
- );
-
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "missing_replica",
- ]);
- assert_eq!(quote["result"]["state"], "unconfigured");
- assert_eq!(quote["result"]["ready_for_quote"], false);
- assert_eq!(
- quote["result"]["issues"][0]["code"],
- "basket_market_replica_missing"
- );
- assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
-}
-
-#[test]
-fn basket_quote_rejects_unresolved_listing_before_order_write() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&["--format", "json", "store", "init"]);
- sandbox.json_success(&["--format", "json", "basket", "create", "unresolved_listing"]);
- let add = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "unresolved_listing",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- assert_eq!(add["result"]["ready_for_quote"], false);
- assert_eq!(
- add["result"]["issues"][0]["code"],
- "basket_item_listing_unresolved"
- );
-
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "unresolved_listing",
- ]);
- assert_eq!(quote["result"]["state"], "unconfigured");
- assert_eq!(quote["result"]["ready_for_quote"], false);
- assert_eq!(
- quote["result"]["issues"][0]["code"],
- "basket_item_listing_unresolved"
- );
- assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
-}
-
-#[test]
-fn basket_quote_rejects_ambiguous_listing_before_order_write() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- duplicate_orderable_listing_row(&sandbox, LISTING_ADDR);
- sandbox.json_success(&["--format", "json", "basket", "create", "ambiguous_listing"]);
- let add = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "ambiguous_listing",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- assert_eq!(add["result"]["ready_for_quote"], false);
- assert_eq!(
- add["result"]["issues"][0]["code"],
- "basket_item_listing_ambiguous"
- );
-
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "ambiguous_listing",
- ]);
- assert_eq!(quote["result"]["state"], "unconfigured");
- assert_eq!(quote["result"]["ready_for_quote"], false);
- assert_eq!(
- quote["result"]["issues"][0]["code"],
- "basket_item_listing_ambiguous"
- );
- assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
-}
-
-#[test]
-fn basket_quote_rejects_invalid_verified_primary_bin_before_order_write() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("missing-bin"));
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "create",
- "invalid_primary_bin",
- ]);
- let add = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "invalid_primary_bin",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- assert_eq!(add["result"]["ready_for_quote"], false);
- assert_eq!(
- add["result"]["issues"][0]["code"],
- "listing_primary_bin_invalid"
- );
-
- let validate = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "validate",
- "invalid_primary_bin",
- ]);
- assert_eq!(validate["result"]["state"], "unconfigured");
- assert_eq!(validate["result"]["ready_for_quote"], false);
- assert_eq!(
- validate["result"]["issues"][0]["code"],
- "listing_primary_bin_invalid"
- );
-
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "invalid_primary_bin",
- ]);
- assert_eq!(quote["result"]["state"], "unconfigured");
- assert_eq!(quote["result"]["ready_for_quote"], false);
- assert_eq!(
- quote["result"]["issues"][0]["code"],
- "listing_primary_bin_invalid"
- );
- assert!(!sandbox.root().join("data/apps/cli/orders/drafts").exists());
-}
-
-#[test]
-fn order_submit_dry_run_does_not_use_local_primary_bin_preflight() {
- let sandbox = RadrootsCliSandbox::new();
- let order_id = create_ready_order(&sandbox, "stale_invalid_bin");
- update_orderable_listing_primary_bin_id(&sandbox, LISTING_ADDR, Some("missing-bin"));
-
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "trade",
- "submit",
- &order_id,
- ]);
-
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["source"], "SDK trade submit · local key");
- assert_no_removed_command_reference(&value, &["trade", "submit", "--dry-run"]);
- assert_no_daemon_runtime_reference(&value, &["trade", "submit", "--dry-run"]);
-}
-
-#[test]
-fn order_submit_dry_run_does_not_use_local_quantity_preflight() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
- seed_orderable_listing(&sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
- sandbox.json_success(&["--format", "json", "basket", "create", "dry_over_quantity"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "dry_over_quantity",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "6",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "dry_over_quantity",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
-
- let value =
- sandbox.json_success(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
-
- assert_eq!(value["operation_id"], "trade.submit");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["result"]["state"], "dry_run");
- assert_eq!(value["result"]["source"], "SDK trade submit · local key");
-}
-
-#[test]
-fn ready_order_submit_dry_run_validates_local_buyer_authority() {
- let sandbox = RadrootsCliSandbox::new();
- let first = sandbox.json_success(&["--format", "json", "account", "create"]);
- let first_account_id = first["result"]["account"]["id"]
- .as_str()
- .expect("first account id");
- configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
- let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
- seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
- sandbox.json_success(&["--format", "json", "basket", "create", "ready_order"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "item",
- "add",
- "ready_order",
- "--listing-addr",
- LISTING_ADDR,
- "--bin-id",
- "bin-1",
- "--quantity",
- "2",
- ]);
- let quote = sandbox.json_success(&[
- "--format",
- "json",
- "basket",
- "quote",
- "create",
- "ready_order",
- ]);
- let order_id = quote["result"]["quote"]["trade_id"]
- .as_str()
- .expect("order id");
- assert_eq!(quote["result"]["quote"]["ready_for_submit"], true);
- assert_eq!(
- quote["result"]["trade"]["buyer_account_id"],
- first_account_id
- );
- assert_eq!(
- quote["result"]["trade"]["listing_event_id"],
- listing_event_id
- );
-
- let dry_run =
- sandbox.json_success(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert_eq!(dry_run["operation_id"], "trade.submit");
- assert_eq!(dry_run["dry_run"], true);
- assert_eq!(dry_run["result"]["state"], "dry_run");
- assert_eq!(dry_run["result"]["source"], "SDK trade submit · local key");
- assert_eq!(dry_run["result"]["event_kind"], 3422);
- assert_eq!(
- dry_run["result"]["target_transport_endpoints"][0],
- ORDERABLE_LISTING_RELAY
- );
- assert_no_daemon_runtime_reference(&dry_run, &["trade", "submit", "--dry-run"]);
-
- let second = sandbox.json_success(&["--format", "json", "account", "create"]);
- let second_account_id = second["result"]["account"]["id"]
- .as_str()
- .expect("second account id");
- sandbox.json_success(&[
- "--format",
- "json",
- "account",
- "selection",
- "update",
- second_account_id,
- ]);
- let drift =
- sandbox.json_success(&["--format", "json", "--dry-run", "trade", "submit", order_id]);
- assert_eq!(drift["operation_id"], "trade.submit");
- assert_eq!(drift["result"]["state"], "dry_run");
- assert_eq!(drift["result"]["buyer_account_id"], first_account_id);
-
- let (output, mismatch) = sandbox.json_output(&[
- "--format",
- "json",
- "--account-id",
- second_account_id,
- "--dry-run",
- "trade",
- "submit",
- order_id,
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(5));
- assert_eq!(mismatch["operation_id"], "trade.submit");
- assert_eq!(mismatch["errors"][0]["code"], "account_mismatch");
- assert_eq!(mismatch["errors"][0]["detail"]["class"], "account");
- assert_no_removed_command_reference(&mismatch, &["trade", "submit", "--dry-run"]);
- assert_no_daemon_runtime_reference(&mismatch, &["trade", "submit", "--dry-run"]);
-
- let (network_output, network_mismatch) = sandbox.json_output(&[
- "--format",
- "json",
- "--account-id",
- second_account_id,
- "--approval-token",
- "approve",
- "trade",
- "submit",
- order_id,
- ]);
-
- assert!(!network_output.status.success());
- assert_eq!(network_output.status.code(), Some(5));
- assert_eq!(network_mismatch["operation_id"], "trade.submit");
- assert_eq!(network_mismatch["result"], Value::Null);
- assert_eq!(network_mismatch["errors"][0]["code"], "account_mismatch");
- assert_eq!(network_mismatch["errors"][0]["detail"]["class"], "account");
- assert_no_daemon_runtime_reference(&network_mismatch, &["trade", "submit"]);
-}
-
-#[test]
-fn seller_target_flow_acceptance_uses_target_operations() {
- let sandbox = RadrootsCliSandbox::new();
-
- let account = sandbox.json_success(&["--format", "json", "account", "create"]);
- let account_id = account["result"]["account"]["id"]
- .as_str()
- .expect("account id");
- assert_eq!(account["operation_id"], "account.create");
- assert_eq!(account["result"]["account"]["signer"], "local");
- assert_eq!(account["result"]["account"]["custody"], "secret_backed");
- assert_eq!(account["result"]["account"]["write_capable"], true);
- assert_no_removed_command_reference(&account, &["account", "create"]);
-
- let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
- assert_eq!(signer["operation_id"], "signer.status.get");
- assert_eq!(signer["result"]["mode"], "local");
- assert_eq!(signer["result"]["state"], "ready");
- assert_eq!(signer["result"]["signer_account_id"], account_id);
- assert_no_removed_command_reference(&signer, &["signer", "status", "get"]);
-
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Green Farm",
- "--location",
- "farmstand",
- "--city",
- "San Francisco",
- "--country",
- "US",
- "--geohash",
- "9q8yy",
- "--delivery-method",
- "pickup",
- ]);
- assert_eq!(farm["operation_id"], "farm.create");
- assert_eq!(farm["result"]["state"], "saved");
- assert_no_removed_command_reference(&farm, &["farm", "create"]);
-
- let create = sandbox.json_success(&[
- "--format",
- "json",
- "listing",
- "create",
- "--key",
- "eggs",
- "--title",
- "Eggs",
- "--category",
- "eggs",
- "--summary",
- "Fresh eggs",
- "--bin-id",
- "bin-1",
- "--quantity-amount",
- "1",
- "--quantity-unit",
- "each",
- "--price-amount",
- "6",
- "--price-currency",
- "USD",
- "--price-per-amount",
- "1",
- "--price-per-unit",
- "each",
- "--available",
- "10",
- ]);
- let listing_file = create["result"]["file"].as_str().expect("listing file");
- assert_eq!(create["operation_id"], "listing.create");
- assert!(Path::new(listing_file).exists());
- assert_no_removed_command_reference(&create, &["listing", "create"]);
-
- let list = sandbox.json_success(&["--format", "json", "listing", "list"]);
- assert_eq!(list["operation_id"], "listing.list");
- assert_eq!(list["result"]["state"], "ready");
- assert_eq!(list["result"]["count"], 1);
- assert_eq!(
- list["result"]["listings"][0]["id"],
- create["result"]["listing_id"]
- );
- assert_eq!(list["result"]["listings"][0]["state"], "ready");
- assert_no_removed_command_reference(&list, &["listing", "list"]);
-
- let validate = sandbox.json_success(&["--format", "json", "listing", "validate", listing_file]);
- assert_eq!(validate["operation_id"], "listing.validate");
- assert_eq!(validate["result"]["valid"], true);
- assert_eq!(validate["result"]["issues"], Value::Null);
- assert_no_removed_command_reference(&validate, &["listing", "validate"]);
-
- let publish = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "publish",
- listing_file,
- ]);
- assert_eq!(publish["operation_id"], "listing.publish");
- assert_eq!(publish["result"]["state"], "dry_run");
- assert_no_removed_command_reference(&publish, &["listing", "publish", "--dry-run"]);
- assert_no_daemon_runtime_reference(&publish, &["listing", "publish", "--dry-run"]);
-
- let archive = sandbox.json_success(&[
- "--format",
- "json",
- "--dry-run",
- "listing",
- "archive",
- listing_file,
- ]);
- assert_eq!(archive["operation_id"], "listing.archive");
- assert_eq!(archive["result"]["state"], "dry_run");
- assert_eq!(archive["result"]["operation"], "archive");
- assert_no_removed_command_reference(&archive, &["listing", "archive", "--dry-run"]);
- assert_no_daemon_runtime_reference(&archive, &["listing", "archive", "--dry-run"]);
-
- let (publish_output, unavailable_publish) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file,
- ]);
- assert!(!publish_output.status.success());
- assert_eq!(unavailable_publish["operation_id"], "listing.publish");
- assert_eq!(
- unavailable_publish["errors"][0]["code"],
- "network_unavailable"
- );
- assert_eq!(
- unavailable_publish["errors"][0]["detail"]["class"],
- "network"
- );
- assert_no_removed_command_reference(&unavailable_publish, &["listing", "publish"]);
- assert_no_daemon_runtime_reference(&unavailable_publish, &["listing", "publish"]);
-
- let (archive_output, unavailable_archive) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "archive",
- listing_file,
- ]);
- assert!(!archive_output.status.success());
- assert_eq!(unavailable_archive["operation_id"], "listing.archive");
- assert_eq!(
- unavailable_archive["errors"][0]["code"],
- "network_unavailable"
- );
- assert_eq!(
- unavailable_archive["errors"][0]["detail"]["class"],
- "network"
- );
- assert_no_removed_command_reference(&unavailable_archive, &["listing", "archive"]);
- assert_no_daemon_runtime_reference(&unavailable_archive, &["listing", "archive"]);
}