commit 0a9453fe47d47f6e8b11f8ad39570d72c6cf813d
parent b159e1225161443c327cd0304c4620580e230505
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Apr 2026 03:32:32 +0000
cli: cut over public parser to mvp surface
- route the binary through target operations and output envelopes
- remove legacy dispatcher and renderer from the active binary graph
- replace legacy public-command integration tests with mvp contract coverage
- reject removed commands, removed flags, unsupported ndjson, and missing approval
Diffstat:
| M | src/cli.rs | | | 1305 | +------------------------------------------------------------------------------ |
| M | src/domain/runtime.rs | | | 2 | ++ |
| M | src/main.rs | | | 361 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- |
| M | src/runtime/mod.rs | | | 2 | ++ |
| M | src/target_cli.rs | | | 2 | +- |
| D | tests/doctor.rs | | | 154 | ------------------------------------------------------------------------------- |
| D | tests/farm.rs | | | 274 | ------------------------------------------------------------------------------- |
| D | tests/find.rs | | | 541 | ------------------------------------------------------------------------------- |
| D | tests/help.rs | | | 168 | ------------------------------------------------------------------------------- |
| D | tests/identity_commands.rs | | | 601 | ------------------------------------------------------------------------------- |
| D | tests/job_rpc.rs | | | 888 | ------------------------------------------------------------------------------- |
| D | tests/listing.rs | | | 1860 | ------------------------------------------------------------------------------- |
| D | tests/local.rs | | | 164 | ------------------------------------------------------------------------------- |
| D | tests/market.rs | | | 437 | ------------------------------------------------------------------------------- |
| D | tests/myc_status.rs | | | 922 | ------------------------------------------------------------------------------- |
| D | tests/order.rs | | | 1817 | ------------------------------------------------------------------------------- |
| D | tests/relay_net.rs | | | 154 | ------------------------------------------------------------------------------- |
| D | tests/runtime_management.rs | | | 367 | ------------------------------------------------------------------------------- |
| D | tests/runtime_show.rs | | | 873 | ------------------------------------------------------------------------------- |
| D | tests/sell.rs | | | 414 | ------------------------------------------------------------------------------- |
| D | tests/signer_status.rs | | | 201 | ------------------------------------------------------------------------------- |
| D | tests/sync.rs | | | 200 | ------------------------------------------------------------------------------- |
| A | tests/target_cli.rs | | | 137 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | tests/workflow.rs | | | 274 | ------------------------------------------------------------------------------- |
24 files changed, 472 insertions(+), 11646 deletions(-)
diff --git a/src/cli.rs b/src/cli.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)]
+
use clap::{
ArgAction, Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum,
error::ErrorKind,
@@ -24,142 +26,9 @@ impl OutputFormatArg {
}
}
-const ROOT_HELP: &str = "\
-radroots - local food trade on Nostr
-
-Start here
- setup Guided first-time setup for sellers and buyers
- status Show what is ready and what needs attention
-
-Sell from your farm
- farm Set up and publish your farm
- sell Create, check, and publish listings
-
-Buy from the market
- market Update local market data and search listings
- order Create and manage order requests
-
-Accounts and settings
- account Create, import, and manage local accounts
- config Show effective configuration
-
-Advanced and troubleshooting
- doctor Check readiness and suggest next steps
- local Manage local market data storage
- sync Inspect sync status and watch updates
- relay Show relay configuration
- net Show network posture
- signer Show signer readiness
- rpc Show runtime bridge status
- myc Show myc status
- runtime Manage runtimes
- job Inspect background jobs
- listing Advanced listing commands
- find Advanced search command
-
-Global options
- --output <human|json|ndjson>
- --json
- --ndjson
- --dry-run
- --no-input
- --yes
- --quiet
- --verbose
- --trace
- --no-color
- --account <ACCOUNT>
- --signer <SIGNER>
- --relay <RELAY>
- --myc-status-timeout-ms <MS>
-
-Examples
- radroots setup seller
- radroots market search eggs
- radroots sell add tomatoes
- radroots order create --listing sf-tomatoes --bin bin-1 --qty 2
-";
-
-const SETUP_HELP: &str = "\
-Examples:
- radroots setup seller
- radroots setup buyer
- radroots setup both
-
-This workflow layer sits on top of the existing account, local, and farm commands.
-Use `radroots account create` or `radroots account select` explicitly when no actor is resolved.
-";
-
-const STATUS_HELP: &str = "\
-Examples:
- radroots status
- radroots doctor
- radroots config show
-
-This workflow summary reflects the current readiness and configuration surfaces.
-When no actor is resolved, it points to explicit account commands instead of mutating account state.
-";
-
-const ACCOUNT_HELP: &str = "\
-Examples:
- radroots account create
- radroots account import ./identity.json
- radroots account view
- radroots account list
- radroots account select market-main
- radroots account clear-default
- radroots account remove market-main
-
-Select stores the default account. Clear-default removes the stored default without deleting accounts.
-
-Compatibility aliases: new, whoami, ls, use.
-";
-
-const FARM_HELP: &str = "\
-Examples:
- radroots farm init
- radroots farm set delivery pickup
- radroots farm check
- radroots farm show --scope workspace
- radroots farm publish
-
-Compatibility paths: `farm setup`, `farm status`, and `farm get` remain available.
-";
-
-const MARKET_HELP: &str = "\
-Examples:
- radroots market update
- radroots market search tomatoes
- radroots market view sf-tomatoes
-
-Compatibility paths: `sync pull`, `find`, and `listing get` remain available.
-";
-
-const SELL_HELP: &str = "\
-Examples:
- radroots sell add tomatoes --pack \"1 kg\" --price \"10 USD/kg\" --stock 25
- radroots sell check ./listing.toml
- radroots sell publish ./listing.toml
-
-Compatibility path: the advanced `listing` command family remains available.
-";
-
-const ORDER_HELP: &str = "\
-Examples:
- radroots order create --listing sf-tomatoes --bin bin-1 --qty 2
- radroots order view ord_demo
- radroots order list
- radroots order submit ord_demo --watch
-
-Compatibility aliases: new, get, ls.
-";
-
#[derive(Debug, Parser, Clone)]
#[command(name = "radroots")]
#[command(version)]
-#[command(
- after_help = "Global output: use --output <human|json|ndjson>. Existing --json and --ndjson aliases remain supported."
-)]
pub struct CliArgs {
#[arg(skip)]
pub output_format: Option<OutputFormatArg>,
@@ -252,14 +121,6 @@ impl CliArgs {
pub fn build_command() -> clap::Command {
<Self as CommandFactory>::command()
- .override_help(ROOT_HELP)
- .mut_subcommand("setup", |command| command.after_help(SETUP_HELP))
- .mut_subcommand("status", |command| command.after_help(STATUS_HELP))
- .mut_subcommand("account", |command| command.after_help(ACCOUNT_HELP))
- .mut_subcommand("farm", |command| command.after_help(FARM_HELP))
- .mut_subcommand("market", |command| command.after_help(MARKET_HELP))
- .mut_subcommand("sell", |command| command.after_help(SELL_HELP))
- .mut_subcommand("order", |command| command.after_help(ORDER_HELP))
}
fn command_error(message: impl Into<String>, kind: ErrorKind) -> clap::Error {
@@ -1263,1165 +1124,3 @@ pub struct SellRestockArgs {
pub file: PathBuf,
pub available: String,
}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
-
- use super::{
- AccountCommand, CliArgs, Command, ConfigCommand, FarmCommand, FarmFieldArg, FarmScopeArg,
- JobCommand, JobWatchArgs, ListingCommand, LocalCommand, LocalExportFormatArg,
- MarketCommand, MycCommand, NetCommand, OrderCommand, OrderWatchArgs, OutputFormatArg,
- RelayCommand, RpcCommand, RuntimeCommand, RuntimeConfigCommand, SellCommand, SetupRoleArg,
- SignerCommand, SignerSessionCommand, SyncCommand, SyncWatchArgs,
- };
- use crate::runtime::config::OutputFormat;
- #[test]
- fn parses_config_show_command() {
- let parsed = CliArgs::parse_from(["radroots", "config", "show"]);
- match parsed.command {
- Command::Config(config) => match config.command {
- ConfigCommand::Show => {}
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_global_runtime_flags() {
- let parsed = CliArgs::parse_from([
- "radroots",
- "--output",
- "json",
- "--json",
- "--verbose",
- "--dry-run",
- "--no-color",
- "--no-input",
- "--yes",
- "--env-file",
- ".env.local",
- "--account",
- "acct_demo",
- "--log-filter",
- "debug,radroots_cli=trace",
- "--log-dir",
- "logs",
- "--log-stdout",
- "--identity-path",
- "identity.local.json",
- "--signer",
- "myc",
- "--relay",
- "wss://relay.one",
- "--relay",
- "wss://relay.two",
- "--myc-executable",
- "bin/myc",
- "--myc-status-timeout-ms",
- "2500",
- "--hyf-enabled",
- "--hyf-executable",
- "bin/hyfd",
- "config",
- "show",
- ]);
- assert_eq!(parsed.output_format, Some(OutputFormatArg::Json));
- assert!(parsed.json);
- assert!(parsed.verbose);
- assert!(parsed.dry_run);
- assert!(parsed.no_color);
- assert!(parsed.no_input);
- assert!(parsed.yes);
- assert_eq!(
- parsed.env_file.as_deref().and_then(|path| path.to_str()),
- Some(".env.local")
- );
- assert_eq!(
- parsed.log_filter.as_deref(),
- Some("debug,radroots_cli=trace")
- );
- assert_eq!(
- parsed.log_dir.as_deref().and_then(|path| path.to_str()),
- Some("logs")
- );
- assert_eq!(parsed.account.as_deref(), Some("acct_demo"));
- assert!(parsed.log_stdout);
- assert_eq!(
- parsed
- .identity_path
- .as_deref()
- .and_then(|path| path.to_str()),
- Some("identity.local.json")
- );
- assert_eq!(parsed.signer.as_deref(), Some("myc"));
- assert_eq!(
- parsed.relay,
- vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
- );
- assert_eq!(
- parsed
- .myc_executable
- .as_deref()
- .and_then(|path| path.to_str()),
- Some("bin/myc")
- );
- assert_eq!(parsed.myc_status_timeout_ms, Some(2500));
- assert!(parsed.hyf_enabled);
- assert_eq!(
- parsed
- .hyf_executable
- .as_deref()
- .and_then(|path| path.to_str()),
- Some("bin/hyfd")
- );
- }
-
- #[test]
- fn parses_output_format_and_interaction_flags() {
- let parsed = CliArgs::parse_from([
- "radroots",
- "--output",
- "ndjson",
- "--non-interactive",
- "--yes",
- "config",
- "show",
- ]);
- assert_eq!(parsed.output_format, Some(OutputFormatArg::Ndjson));
- assert!(parsed.no_input);
- assert!(parsed.yes);
- }
-
- #[test]
- fn parses_output_format_after_non_conflicting_subcommand() {
- let parsed = CliArgs::parse_from(["radroots", "config", "show", "--output", "json"]);
- assert_eq!(parsed.output_format, Some(OutputFormatArg::Json));
- match parsed.command {
- Command::Config(config) => match config.command {
- ConfigCommand::Show => {}
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn low_level_output_flags_remain_command_local() {
- let parsed = CliArgs::parse_from([
- "radroots",
- "--output",
- "json",
- "listing",
- "new",
- "--output",
- "listing.toml",
- ]);
- assert_eq!(parsed.output_format, Some(OutputFormatArg::Json));
- match parsed.command {
- Command::Listing(listing) => match listing.command {
- ListingCommand::New(args) => {
- assert_eq!(
- args.output.as_deref().and_then(|path| path.to_str()),
- Some("listing.toml")
- );
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn command_group_output_flag_can_still_target_global_format() {
- let parsed = CliArgs::parse_from([
- "radroots",
- "listing",
- "--output",
- "json",
- "new",
- "--output",
- "listing.toml",
- ]);
- assert_eq!(parsed.output_format, Some(OutputFormatArg::Json));
- match parsed.command {
- Command::Listing(listing) => match listing.command {
- ListingCommand::New(args) => {
- assert_eq!(
- args.output.as_deref().and_then(|path| path.to_str()),
- Some("listing.toml")
- );
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_human_first_top_level_commands() {
- let setup = CliArgs::parse_from(["radroots", "setup", "seller"]);
- match setup.command {
- Command::Setup(args) => assert_eq!(args.role, SetupRoleArg::Seller),
- _ => panic!("unexpected command variant"),
- }
-
- let status = CliArgs::parse_from(["radroots", "status"]);
- assert!(matches!(status.command, Command::Status));
-
- let market = CliArgs::parse_from(["radroots", "market", "search", "eggs"]);
- match market.command {
- Command::Market(args) => match args.command {
- MarketCommand::Search(find) => assert_eq!(find.query, vec!["eggs"]),
- _ => panic!("unexpected market subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let sell = CliArgs::parse_from([
- "radroots",
- "sell",
- "add",
- "eggs",
- "--pack",
- "dozen",
- "--price",
- "8 USD/dozen",
- "--stock",
- "10",
- ]);
- match sell.command {
- Command::Sell(args) => match args.command {
- SellCommand::Add(add) => {
- assert_eq!(add.product, "eggs");
- assert_eq!(add.pack.as_deref(), Some("dozen"));
- assert_eq!(add.price_expr.as_deref(), Some("8 USD/dozen"));
- assert_eq!(add.stock.as_deref(), Some("10"));
- }
- _ => panic!("unexpected sell subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_human_first_aliases() {
- let account_create = CliArgs::parse_from(["radroots", "account", "create"]);
- match account_create.command {
- Command::Account(account) => assert!(matches!(account.command, AccountCommand::New)),
- _ => panic!("unexpected command variant"),
- }
-
- let account_view = CliArgs::parse_from(["radroots", "account", "view"]);
- match account_view.command {
- Command::Account(account) => assert!(matches!(account.command, AccountCommand::Whoami)),
- _ => panic!("unexpected command variant"),
- }
-
- let account_list = CliArgs::parse_from(["radroots", "account", "list"]);
- match account_list.command {
- Command::Account(account) => assert!(matches!(account.command, AccountCommand::Ls)),
- _ => panic!("unexpected command variant"),
- }
-
- let account_select = CliArgs::parse_from(["radroots", "account", "select", "market-main"]);
- match account_select.command {
- Command::Account(account) => match account.command {
- AccountCommand::Use(args) => assert_eq!(args.selector, "market-main"),
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let farm_check = CliArgs::parse_from(["radroots", "farm", "check"]);
- match farm_check.command {
- Command::Farm(farm) => assert!(matches!(farm.command, FarmCommand::Status(_))),
- _ => panic!("unexpected command variant"),
- }
-
- let farm_show = CliArgs::parse_from(["radroots", "farm", "show"]);
- match farm_show.command {
- Command::Farm(farm) => assert!(matches!(farm.command, FarmCommand::Get(_))),
- _ => panic!("unexpected command variant"),
- }
-
- let market_view = CliArgs::parse_from(["radroots", "market", "view", "lst_123"]);
- match market_view.command {
- Command::Market(market) => match market.command {
- MarketCommand::View(args) => assert_eq!(args.key, "lst_123"),
- _ => panic!("unexpected market subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_create =
- CliArgs::parse_from(["radroots", "order", "create", "--listing", "eggs"]);
- match order_create.command {
- Command::Order(order) => match order.command {
- OrderCommand::New(args) => assert_eq!(args.listing.as_deref(), Some("eggs")),
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_view = CliArgs::parse_from(["radroots", "order", "view", "ord_demo"]);
- match order_view.command {
- Command::Order(order) => match order.command {
- OrderCommand::Get(args) => assert_eq!(args.key, "ord_demo"),
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_list = CliArgs::parse_from(["radroots", "order", "list"]);
- match order_list.command {
- Command::Order(order) => assert!(matches!(order.command, OrderCommand::Ls)),
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_account_commands() {
- let new = CliArgs::parse_from(["radroots", "account", "new"]);
- match new.command {
- Command::Account(account) => match account.command {
- AccountCommand::New => {}
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let whoami = CliArgs::parse_from(["radroots", "account", "whoami"]);
- match whoami.command {
- Command::Account(account) => match account.command {
- AccountCommand::Whoami => {}
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let ls = CliArgs::parse_from(["radroots", "account", "ls"]);
- match ls.command {
- Command::Account(account) => match account.command {
- AccountCommand::Ls => {}
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let import = CliArgs::parse_from([
- "radroots",
- "account",
- "import",
- "./identity.json",
- "--default",
- ]);
- match import.command {
- Command::Account(account) => match account.command {
- AccountCommand::Import(args) => {
- assert_eq!(args.path, PathBuf::from("./identity.json"));
- assert!(args.default);
- }
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let use_account = CliArgs::parse_from(["radroots", "account", "use", "market-main"]);
- match use_account.command {
- Command::Account(account) => match account.command {
- AccountCommand::Use(args) => assert_eq!(args.selector, "market-main"),
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let clear_default = CliArgs::parse_from(["radroots", "account", "clear-default"]);
- match clear_default.command {
- Command::Account(account) => match account.command {
- AccountCommand::ClearDefault => {}
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let remove = CliArgs::parse_from(["radroots", "account", "remove", "market-main"]);
- match remove.command {
- Command::Account(account) => match account.command {
- AccountCommand::Remove(args) => assert_eq!(args.selector, "market-main"),
- _ => panic!("unexpected account subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_signer_status() {
- let parsed = CliArgs::parse_from(["radroots", "signer", "status"]);
- match parsed.command {
- Command::Signer(signer) => match signer.command {
- SignerCommand::Status => {}
- SignerCommand::Session(_) => panic!("unexpected signer session command"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_signer_session_lifecycle_commands() {
- let parsed = CliArgs::parse_from([
- "radroots",
- "signer",
- "session",
- "require-auth",
- "sess_123",
- "--auth-url",
- "https://auth.example",
- ]);
- match parsed.command {
- Command::Signer(signer) => match signer.command {
- SignerCommand::Session(session) => match session.command {
- SignerSessionCommand::RequireAuth {
- session_id,
- auth_url,
- } => {
- assert_eq!(session_id, "sess_123");
- assert_eq!(auth_url, "https://auth.example");
- }
- _ => panic!("unexpected signer session subcommand"),
- },
- SignerCommand::Status => panic!("unexpected signer status command"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_myc_status() {
- let parsed = CliArgs::parse_from(["radroots", "myc", "status"]);
- match parsed.command {
- Command::Myc(myc) => match myc.command {
- MycCommand::Status => {}
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn parses_v1_command_skeleton() {
- let doctor = CliArgs::parse_from(["radroots", "doctor"]);
- assert!(matches!(doctor.command, Command::Doctor));
-
- let find = CliArgs::parse_from(["radroots", "find", "tomatoes"]);
- match find.command {
- Command::Find(args) => assert_eq!(args.query, vec!["tomatoes"]),
- _ => panic!("unexpected command variant"),
- }
-
- let farm_setup = CliArgs::parse_from([
- "radroots",
- "farm",
- "setup",
- "--scope",
- "workspace",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--city",
- "San Francisco",
- "--region",
- "CA",
- "--country",
- "US",
- "--delivery-method",
- "local_delivery",
- ]);
- match farm_setup.command {
- Command::Farm(args) => match args.command {
- FarmCommand::Setup(setup) => {
- assert_eq!(setup.scope, Some(FarmScopeArg::Workspace));
- assert_eq!(setup.name, "La Huerta");
- assert_eq!(setup.location, "San Francisco, CA");
- assert_eq!(setup.city.as_deref(), Some("San Francisco"));
- assert_eq!(setup.delivery_method, "local_delivery");
- }
- _ => panic!("unexpected farm subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let farm_init = CliArgs::parse_from([
- "radroots",
- "farm",
- "init",
- "--scope",
- "workspace",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--delivery",
- "pickup",
- ]);
- match farm_init.command {
- Command::Farm(args) => match args.command {
- FarmCommand::Init(init) => {
- assert_eq!(init.scope, Some(FarmScopeArg::Workspace));
- assert_eq!(init.name.as_deref(), Some("La Huerta"));
- assert_eq!(init.location.as_deref(), Some("San Francisco, CA"));
- assert_eq!(init.delivery_method.as_deref(), Some("pickup"));
- }
- _ => panic!("unexpected farm subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let farm_set = CliArgs::parse_from([
- "radroots",
- "farm",
- "set",
- "--scope",
- "user",
- "display-name",
- "La",
- "Huerta",
- "Farm",
- ]);
- match farm_set.command {
- Command::Farm(args) => match args.command {
- FarmCommand::Set(set) => {
- assert_eq!(set.scope, Some(FarmScopeArg::User));
- assert_eq!(set.field, FarmFieldArg::DisplayName);
- assert_eq!(set.value, vec!["La", "Huerta", "Farm"]);
- }
- _ => panic!("unexpected farm subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let farm_status = CliArgs::parse_from(["radroots", "farm", "status", "--scope", "user"]);
- match farm_status.command {
- Command::Farm(args) => match args.command {
- FarmCommand::Status(status) => assert_eq!(status.scope, Some(FarmScopeArg::User)),
- _ => panic!("unexpected farm subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let farm_get = CliArgs::parse_from(["radroots", "farm", "get"]);
- match farm_get.command {
- Command::Farm(args) => match args.command {
- FarmCommand::Get(get) => assert!(get.scope.is_none()),
- _ => panic!("unexpected farm subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let farm_publish = CliArgs::parse_from([
- "radroots",
- "farm",
- "publish",
- "--scope",
- "workspace",
- "--idempotency-key",
- "farm-publish-1",
- "--signer-session-id",
- "session-1",
- "--print-job",
- "--print-event",
- ]);
- match farm_publish.command {
- Command::Farm(args) => match args.command {
- FarmCommand::Publish(publish) => {
- assert_eq!(publish.scope, Some(FarmScopeArg::Workspace));
- assert_eq!(publish.idempotency_key.as_deref(), Some("farm-publish-1"));
- assert_eq!(publish.signer_session_id.as_deref(), Some("session-1"));
- assert!(publish.print_job);
- assert!(publish.print_event);
- }
- _ => panic!("unexpected farm subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let relay = CliArgs::parse_from(["radroots", "relay", "ls"]);
- match relay.command {
- Command::Relay(args) => match args.command {
- RelayCommand::Ls => {}
- },
- _ => panic!("unexpected command variant"),
- }
-
- let net = CliArgs::parse_from(["radroots", "net", "status"]);
- match net.command {
- Command::Net(args) => match args.command {
- NetCommand::Status => {}
- },
- _ => panic!("unexpected command variant"),
- }
-
- let local = CliArgs::parse_from(["radroots", "local", "init"]);
- match local.command {
- Command::Local(args) => match args.command {
- LocalCommand::Init => {}
- _ => panic!("unexpected local subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let local_export = CliArgs::parse_from([
- "radroots",
- "local",
- "export",
- "--format",
- "ndjson",
- "--output",
- "replica.ndjson",
- ]);
- match local_export.command {
- Command::Local(args) => match args.command {
- LocalCommand::Export(export) => {
- assert!(matches!(export.format, LocalExportFormatArg::Ndjson));
- assert_eq!(export.output.to_str(), Some("replica.ndjson"));
- }
- _ => panic!("unexpected local subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let sync = CliArgs::parse_from(["radroots", "sync", "status"]);
- match sync.command {
- Command::Sync(args) => match args.command {
- SyncCommand::Status => {}
- _ => panic!("unexpected sync subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let sync_watch = CliArgs::parse_from([
- "radroots",
- "sync",
- "watch",
- "--frames",
- "2",
- "--interval-ms",
- "25",
- ]);
- match sync_watch.command {
- Command::Sync(args) => match args.command {
- SyncCommand::Watch(SyncWatchArgs {
- frames,
- interval_ms,
- }) => {
- assert_eq!(frames, 2);
- assert_eq!(interval_ms, 25);
- }
- _ => panic!("unexpected sync subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let listing_new = CliArgs::parse_from([
- "radroots",
- "listing",
- "new",
- "--output",
- "draft.toml",
- "--key",
- "sf-tomatoes",
- "--title",
- "San Francisco Tomatoes",
- "--category",
- "produce.vegetables.tomatoes",
- "--summary",
- "Fresh tomatoes",
- "--quantity-amount",
- "1000",
- "--quantity-unit",
- "g",
- "--price-amount",
- "0.01",
- "--available",
- "25",
- ]);
- match listing_new.command {
- Command::Listing(args) => match args.command {
- ListingCommand::New(new) => {
- assert_eq!(
- new.output.as_deref().and_then(|path| path.to_str()),
- Some("draft.toml")
- );
- assert_eq!(new.key.as_deref(), Some("sf-tomatoes"));
- assert_eq!(new.title.as_deref(), Some("San Francisco Tomatoes"));
- assert_eq!(new.category.as_deref(), Some("produce.vegetables.tomatoes"));
- assert_eq!(new.summary.as_deref(), Some("Fresh tomatoes"));
- assert_eq!(new.quantity_amount.as_deref(), Some("1000"));
- assert_eq!(new.quantity_unit.as_deref(), Some("g"));
- assert_eq!(new.price_amount.as_deref(), Some("0.01"));
- assert_eq!(new.available.as_deref(), Some("25"));
- assert!(new.price_currency.is_none());
- assert!(new.price_per_amount.is_none());
- assert!(new.price_per_unit.is_none());
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let listing_validate =
- CliArgs::parse_from(["radroots", "listing", "validate", "draft.toml"]);
- match listing_validate.command {
- Command::Listing(args) => match args.command {
- ListingCommand::Validate(file) => {
- assert_eq!(file.file.to_str(), Some("draft.toml"));
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let listing_publish = CliArgs::parse_from(["radroots", "listing", "publish", "draft.toml"]);
- match listing_publish.command {
- Command::Listing(args) => match args.command {
- ListingCommand::Publish(file) => {
- assert_eq!(file.file.to_str(), Some("draft.toml"));
- assert!(file.idempotency_key.is_none());
- assert!(file.signer_session_id.is_none());
- assert!(!file.print_job);
- assert!(!file.print_event);
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let listing_archive = CliArgs::parse_from([
- "radroots",
- "listing",
- "archive",
- "--idempotency-key",
- "archive-key",
- "--print-job",
- "--print-event",
- "draft.toml",
- ]);
- match listing_archive.command {
- Command::Listing(args) => match args.command {
- ListingCommand::Archive(file) => {
- assert_eq!(file.file.to_str(), Some("draft.toml"));
- assert_eq!(file.idempotency_key.as_deref(), Some("archive-key"));
- assert!(file.signer_session_id.is_none());
- assert!(file.print_job);
- assert!(file.print_event);
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let listing_update = CliArgs::parse_from([
- "radroots",
- "listing",
- "update",
- "--signer-session-id",
- "sess_123",
- "draft.toml",
- ]);
- match listing_update.command {
- Command::Listing(args) => match args.command {
- ListingCommand::Update(file) => {
- assert_eq!(file.file.to_str(), Some("draft.toml"));
- assert_eq!(file.signer_session_id.as_deref(), Some("sess_123"));
- }
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let listing_get = CliArgs::parse_from(["radroots", "listing", "get", "lst_123"]);
- match listing_get.command {
- Command::Listing(args) => match args.command {
- ListingCommand::Get(key) => assert_eq!(key.key, "lst_123"),
- _ => panic!("unexpected listing subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let job = CliArgs::parse_from(["radroots", "job", "get", "job_123"]);
- match job.command {
- Command::Job(args) => match args.command {
- JobCommand::Get(key) => assert_eq!(key.key, "job_123"),
- _ => panic!("unexpected job subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let job_watch = CliArgs::parse_from([
- "radroots",
- "job",
- "watch",
- "job_123",
- "--frames",
- "2",
- "--interval-ms",
- "5",
- ]);
- match job_watch.command {
- Command::Job(args) => match args.command {
- JobCommand::Watch(JobWatchArgs {
- key,
- frames,
- interval_ms,
- }) => {
- assert_eq!(key, "job_123");
- assert_eq!(frames, Some(2));
- assert_eq!(interval_ms, 5);
- }
- _ => panic!("unexpected job watch subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let rpc = CliArgs::parse_from(["radroots", "rpc", "status"]);
- match rpc.command {
- Command::Rpc(args) => match args.command {
- RpcCommand::Status => {}
- _ => panic!("unexpected rpc subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let runtime_status = CliArgs::parse_from(["radroots", "runtime", "status", "radrootsd"]);
- match runtime_status.command {
- Command::Runtime(args) => match args.command {
- RuntimeCommand::Status(target) => {
- assert_eq!(target.runtime, "radrootsd");
- assert!(target.instance.is_none());
- }
- _ => panic!("unexpected runtime subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let runtime_logs = CliArgs::parse_from([
- "radroots",
- "runtime",
- "logs",
- "radrootsd",
- "--instance",
- "local",
- ]);
- match runtime_logs.command {
- Command::Runtime(args) => match args.command {
- RuntimeCommand::Logs(target) => {
- assert_eq!(target.runtime, "radrootsd");
- assert_eq!(target.instance.as_deref(), Some("local"));
- }
- _ => panic!("unexpected runtime subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let runtime_config_show =
- CliArgs::parse_from(["radroots", "runtime", "config", "show", "radrootsd"]);
- match runtime_config_show.command {
- Command::Runtime(args) => match args.command {
- RuntimeCommand::Config(runtime_config) => match runtime_config.command {
- RuntimeConfigCommand::Show(target) => {
- assert_eq!(target.runtime, "radrootsd");
- assert!(target.instance.is_none());
- }
- _ => panic!("unexpected runtime config subcommand"),
- },
- _ => panic!("unexpected runtime subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let runtime_config_set = CliArgs::parse_from([
- "radroots",
- "runtime",
- "config",
- "set",
- "radrootsd",
- "--instance",
- "local",
- "bridge.enabled",
- "true",
- ]);
- match runtime_config_set.command {
- Command::Runtime(args) => match args.command {
- RuntimeCommand::Config(runtime_config) => match runtime_config.command {
- RuntimeConfigCommand::Set(set) => {
- assert_eq!(set.target.runtime, "radrootsd");
- assert_eq!(set.target.instance.as_deref(), Some("local"));
- assert_eq!(set.key, "bridge.enabled");
- assert_eq!(set.value, "true");
- }
- _ => panic!("unexpected runtime config subcommand"),
- },
- _ => panic!("unexpected runtime subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_new = CliArgs::parse_from([
- "radroots",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- "--qty",
- "2",
- ]);
- match order_new.command {
- Command::Order(args) => match args.command {
- OrderCommand::New(new) => {
- assert_eq!(new.listing.as_deref(), Some("pasture-eggs"));
- assert_eq!(
- new.listing_addr.as_deref(),
- Some("30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(new.bin_id.as_deref(), Some("bin-1"));
- assert_eq!(new.bin_count, Some(2));
- }
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_create = CliArgs::parse_from(["radroots", "order", "create"]);
- match order_create.command {
- Command::Order(args) => match args.command {
- OrderCommand::New(_) => {}
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_get = CliArgs::parse_from(["radroots", "order", "get", "ord_demo"]);
- match order_get.command {
- Command::Order(args) => match args.command {
- OrderCommand::Get(key) => assert_eq!(key.key, "ord_demo"),
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_view = CliArgs::parse_from(["radroots", "order", "view", "ord_demo"]);
- match order_view.command {
- Command::Order(args) => match args.command {
- OrderCommand::Get(key) => assert_eq!(key.key, "ord_demo"),
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_ls = CliArgs::parse_from(["radroots", "order", "ls"]);
- match order_ls.command {
- Command::Order(args) => match args.command {
- OrderCommand::Ls => {}
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_list = CliArgs::parse_from(["radroots", "order", "list"]);
- match order_list.command {
- Command::Order(args) => match args.command {
- OrderCommand::Ls => {}
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_submit = CliArgs::parse_from([
- "radroots",
- "order",
- "submit",
- "ord_demo",
- "--watch",
- "--idempotency-key",
- "submit-1",
- "--signer-session-id",
- "sess_456",
- ]);
- match order_submit.command {
- Command::Order(args) => match args.command {
- OrderCommand::Submit(submit) => {
- assert_eq!(submit.key, "ord_demo");
- assert!(submit.watch);
- assert_eq!(submit.idempotency_key.as_deref(), Some("submit-1"));
- assert_eq!(submit.signer_session_id.as_deref(), Some("sess_456"));
- }
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
-
- let order_watch = CliArgs::parse_from([
- "radroots",
- "order",
- "watch",
- "ord_demo",
- "--frames",
- "3",
- "--interval-ms",
- "25",
- ]);
- match order_watch.command {
- Command::Order(args) => match args.command {
- OrderCommand::Watch(OrderWatchArgs {
- key,
- frames,
- interval_ms,
- }) => {
- assert_eq!(key, "ord_demo");
- assert_eq!(frames, Some(3));
- assert_eq!(interval_ms, 25);
- }
- _ => panic!("unexpected order subcommand"),
- },
- _ => panic!("unexpected command variant"),
- }
- }
-
- #[test]
- fn command_contract_helpers_report_supported_modes() {
- let config_show = CliArgs::parse_from(["radroots", "config", "show"]);
- assert!(
- config_show
- .command
- .supports_output_format(OutputFormat::Human)
- );
- assert!(
- config_show
- .command
- .supports_output_format(OutputFormat::Json)
- );
- assert!(
- !config_show
- .command
- .supports_output_format(OutputFormat::Ndjson)
- );
- assert!(config_show.command.supports_dry_run());
-
- let account_create = CliArgs::parse_from(["radroots", "account", "create"]);
- assert_eq!(account_create.command.display_name(), "account create");
- assert!(!account_create.command.supports_dry_run());
-
- let account_import =
- CliArgs::parse_from(["radroots", "account", "import", "./identity.json"]);
- assert_eq!(account_import.command.display_name(), "account import");
- assert!(!account_import.command.supports_dry_run());
-
- let account_clear_default = CliArgs::parse_from(["radroots", "account", "clear-default"]);
- assert_eq!(
- account_clear_default.command.display_name(),
- "account clear-default"
- );
- assert!(!account_clear_default.command.supports_dry_run());
-
- let account_remove = CliArgs::parse_from(["radroots", "account", "remove", "market-main"]);
- assert_eq!(account_remove.command.display_name(), "account remove");
- assert!(!account_remove.command.supports_dry_run());
-
- let farm_init = CliArgs::parse_from(["radroots", "farm", "init"]);
- assert_eq!(farm_init.command.display_name(), "farm init");
- assert!(!farm_init.command.supports_dry_run());
-
- let farm_set = CliArgs::parse_from(["radroots", "farm", "set", "name", "La Huerta"]);
- assert_eq!(farm_set.command.display_name(), "farm set");
- assert!(!farm_set.command.supports_dry_run());
-
- let farm_setup = CliArgs::parse_from([
- "radroots",
- "farm",
- "setup",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- ]);
- assert_eq!(farm_setup.command.display_name(), "farm setup");
- assert!(!farm_setup.command.supports_dry_run());
-
- let farm_check = CliArgs::parse_from(["radroots", "farm", "check"]);
- assert_eq!(farm_check.command.display_name(), "farm check");
- assert!(farm_check.command.supports_dry_run());
- assert!(
- !farm_check
- .command
- .supports_output_format(OutputFormat::Ndjson)
- );
-
- let farm_publish = CliArgs::parse_from(["radroots", "farm", "publish"]);
- assert_eq!(farm_publish.command.display_name(), "farm publish");
- assert!(farm_publish.command.supports_dry_run());
-
- let find = CliArgs::parse_from(["radroots", "find", "eggs"]);
- assert!(find.command.supports_output_format(OutputFormat::Ndjson));
-
- let market_search = CliArgs::parse_from(["radroots", "market", "search", "eggs"]);
- assert_eq!(market_search.command.display_name(), "market search");
- assert!(
- market_search
- .command
- .supports_output_format(OutputFormat::Ndjson)
- );
-
- let sync_watch = CliArgs::parse_from(["radroots", "sync", "watch", "--frames", "1"]);
- assert!(
- sync_watch
- .command
- .supports_output_format(OutputFormat::Ndjson)
- );
-
- let sell_add = CliArgs::parse_from(["radroots", "sell", "add", "tomatoes"]);
- assert_eq!(sell_add.command.display_name(), "sell add");
- assert!(!sell_add.command.supports_dry_run());
-
- let order_create = CliArgs::parse_from(["radroots", "order", "create"]);
- assert_eq!(order_create.command.display_name(), "order create");
- assert!(!order_create.command.supports_dry_run());
-
- let order_view = CliArgs::parse_from(["radroots", "order", "view", "ord_demo"]);
- assert_eq!(order_view.command.display_name(), "order view");
- assert!(order_view.command.supports_dry_run());
-
- let order_list = CliArgs::parse_from(["radroots", "order", "list"]);
- assert_eq!(order_list.command.display_name(), "order list");
- assert!(order_list.command.supports_dry_run());
- let order_watch = CliArgs::parse_from(["radroots", "order", "watch", "ord_demo"]);
- assert!(
- order_watch
- .command
- .supports_output_format(OutputFormat::Ndjson)
- );
-
- let order_submit = CliArgs::parse_from(["radroots", "order", "submit", "ord_demo"]);
- assert_eq!(order_submit.command.display_name(), "order submit");
- assert!(order_submit.command.supports_dry_run());
-
- let setup = CliArgs::parse_from(["radroots", "setup", "buyer"]);
- assert_eq!(setup.command.display_name(), "setup buyer");
- assert!(!setup.command.supports_dry_run());
-
- let status = CliArgs::parse_from(["radroots", "status"]);
- assert_eq!(status.command.display_name(), "status");
- assert!(status.command.supports_dry_run());
-
- let runtime_status = CliArgs::parse_from(["radroots", "runtime", "status", "radrootsd"]);
- assert_eq!(runtime_status.command.display_name(), "runtime status");
- assert!(runtime_status.command.supports_dry_run());
- assert!(
- !runtime_status
- .command
- .supports_output_format(OutputFormat::Ndjson)
- );
- }
-}
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)]
+
use std::process::ExitCode;
use radroots_events::farm::RadrootsFarm;
diff --git a/src/main.rs b/src/main.rs
@@ -1,7 +1,6 @@
#![forbid(unsafe_code)]
mod cli;
-mod commands;
mod domain;
mod operation_adapter;
mod operation_basket;
@@ -13,18 +12,30 @@ mod operation_order;
mod operation_registry;
mod operation_runtime;
mod output_contract;
-mod render;
mod runtime;
mod target_cli;
use std::io::Write;
use std::process::ExitCode;
-use crate::cli::CliArgs;
-use crate::commands::dispatch;
-use crate::render::render_output;
-use crate::runtime::config::{OutputFormat, RuntimeConfig};
+use clap::Parser;
+
+use crate::cli::{CliArgs, Command, ConfigArgs, ConfigCommand, OutputFormatArg};
+use crate::operation_adapter::{
+ MvpOperationRequest, OperationAdapter, OperationAdapterError, OperationOutputFormat,
+ OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService,
+};
+use crate::operation_basket::BasketOperationService;
+use crate::operation_core::CoreOperationService;
+use crate::operation_farm::FarmOperationService;
+use crate::operation_listing::ListingOperationService;
+use crate::operation_market::MarketOperationService;
+use crate::operation_order::OrderOperationService;
+use crate::operation_runtime::RuntimeOperationService;
+use crate::output_contract::OutputEnvelope;
+use crate::runtime::config::RuntimeConfig;
use crate::runtime::logging::initialize_logging;
+use crate::target_cli::{TargetCliArgs, TargetOutputFormat};
fn main() -> ExitCode {
match run() {
@@ -39,43 +50,327 @@ fn main() -> ExitCode {
fn run() -> Result<ExitCode, runtime::RuntimeError> {
debug_assert!(operation_registry::registry_linkage_is_valid());
debug_assert!(operation_adapter::adapter_registry_linkage_is_valid());
- let args = CliArgs::parse();
- let config = RuntimeConfig::from_system(&args)?;
- validate_command_contracts(&args.command, &config)?;
+ let args = TargetCliArgs::parse();
+ let config = RuntimeConfig::from_system(&config_args_from_target(&args)?)?;
let logging = initialize_logging(&config.logging)?;
- let output = dispatch(&args.command, &config, &logging)?;
- render_output(&output, &config.output)?;
- Ok(output.exit_code())
+ let request = MvpOperationRequest::from_target_args(&args).map_err(operation_config_error)?;
+ let envelope = match validate_request_contract(&request) {
+ Ok(()) => execute_request(request, &config, &logging),
+ Err(error) => failure_envelope(&request, error),
+ };
+ render_envelope(&envelope, args.format)?;
+ Ok(envelope_exit_code(&envelope))
+}
+
+fn config_args_from_target(args: &TargetCliArgs) -> Result<CliArgs, runtime::RuntimeError> {
+ Ok(CliArgs {
+ output_format: Some(match args.format {
+ TargetOutputFormat::Human => OutputFormatArg::Human,
+ TargetOutputFormat::Json => OutputFormatArg::Json,
+ TargetOutputFormat::Ndjson => OutputFormatArg::Ndjson,
+ }),
+ json: false,
+ ndjson: false,
+ env_file: None,
+ quiet: args.quiet,
+ verbose: args.verbose,
+ trace: args.trace,
+ dry_run: args.dry_run,
+ no_color: args.no_color,
+ no_input: args.no_input,
+ yes: false,
+ log_filter: None,
+ log_dir: None,
+ log_stdout: false,
+ no_log_stdout: false,
+ account: args.account_id.clone(),
+ identity_path: None,
+ signer: None,
+ relay: args.relay.clone(),
+ myc_executable: None,
+ myc_status_timeout_ms: None,
+ hyf_enabled: false,
+ no_hyf_enabled: false,
+ hyf_executable: None,
+ command: Command::Config(ConfigArgs {
+ command: ConfigCommand::Show,
+ }),
+ })
}
-fn validate_command_contracts(
- command: &crate::cli::Command,
+fn execute_request(
+ request: MvpOperationRequest,
config: &RuntimeConfig,
-) -> Result<(), runtime::RuntimeError> {
- if let crate::cli::Command::Order(order) = command {
- if let crate::cli::OrderCommand::Submit(args) = &order.command {
- if args.watch && config.output.format != OutputFormat::Human {
- return Err(runtime::RuntimeError::Config(
- "`order submit --watch` only supports human output; use `order submit` and `order watch` for machine output".to_owned(),
- ));
- }
+ logging: &runtime::logging::LoggingState,
+) -> OutputEnvelope {
+ match request {
+ MvpOperationRequest::WorkspaceInit(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::WorkspaceGet(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::HealthStatusGet(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::HealthCheckRun(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::ConfigGet(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountCreate(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountImport(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountGet(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountList(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountRemove(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountSelectionGet(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountSelectionUpdate(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::AccountSelectionClear(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::StoreInit(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::StoreStatusGet(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::StoreExport(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::StoreBackupCreate(request) => {
+ execute_with(CoreOperationService::new(config, logging), request)
+ }
+ MvpOperationRequest::SignerStatusGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RelayList(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::SyncStatusGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::SyncPull(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::SyncPush(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::SyncWatch(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RuntimeStatusGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RuntimeStart(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RuntimeStop(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RuntimeRestart(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RuntimeLogWatch(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::RuntimeConfigGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::JobGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::JobList(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::JobWatch(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmCreate(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmGet(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmProfileUpdate(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmLocationUpdate(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmFulfillmentUpdate(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmReadinessCheck(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::FarmPublish(request) => {
+ execute_with(FarmOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingCreate(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingGet(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingList(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingUpdate(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingValidate(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingPublish(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::ListingArchive(request) => {
+ execute_with(ListingOperationService::new(config), request)
+ }
+ MvpOperationRequest::MarketRefresh(request) => {
+ execute_with(MarketOperationService::new(config), request)
+ }
+ MvpOperationRequest::MarketProductSearch(request) => {
+ execute_with(MarketOperationService::new(config), request)
+ }
+ MvpOperationRequest::MarketListingGet(request) => {
+ execute_with(MarketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketCreate(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketGet(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketList(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketItemAdd(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketItemUpdate(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketItemRemove(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketValidate(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::BasketQuoteCreate(request) => {
+ execute_with(BasketOperationService::new(config), request)
+ }
+ MvpOperationRequest::OrderSubmit(request) => {
+ execute_with(OrderOperationService::new(config), request)
+ }
+ MvpOperationRequest::OrderGet(request) => {
+ execute_with(OrderOperationService::new(config), request)
+ }
+ MvpOperationRequest::OrderList(request) => {
+ execute_with(OrderOperationService::new(config), request)
+ }
+ MvpOperationRequest::OrderEventList(request) => {
+ execute_with(OrderOperationService::new(config), request)
+ }
+ MvpOperationRequest::OrderEventWatch(request) => {
+ execute_with(OrderOperationService::new(config), request)
}
}
+}
- if !command.supports_output_format(config.output.format) {
- return Err(runtime::RuntimeError::Config(format!(
- "`{}` does not support --{}",
- command.display_name(),
- config.output.format.as_str()
- )));
+fn execute_with<S, P>(service: S, request: OperationRequest<P>) -> OutputEnvelope
+where
+ S: OperationService<P>,
+ P: OperationRequestPayload,
+ S::Result: OperationResultPayload,
+{
+ let operation_id = request.operation_id().to_owned();
+ let envelope_context = request
+ .context
+ .envelope_context(format!("req_{}", operation_id.replace('.', "_")));
+ match OperationAdapter::new(service)
+ .execute(request)
+ .and_then(|result| result.to_envelope(envelope_context.clone()))
+ {
+ Ok(envelope) => envelope,
+ Err(error) => {
+ OutputEnvelope::failure(operation_id, error.to_output_error(), envelope_context)
+ }
}
+}
- if config.output.dry_run && !command.supports_dry_run() {
- return Err(runtime::RuntimeError::Config(format!(
- "`{}` does not support --dry-run yet",
- command.display_name()
- )));
+fn validate_request_contract(request: &MvpOperationRequest) -> Result<(), OperationAdapterError> {
+ let spec = request.spec();
+ if matches!(
+ request.context().output_format,
+ OperationOutputFormat::Ndjson
+ ) && !spec.supports_ndjson
+ {
+ return Err(OperationAdapterError::InvalidInput {
+ operation_id: spec.operation_id.to_owned(),
+ message: format!("`{}` does not support --format ndjson", spec.cli_path),
+ });
+ }
+ 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),
+ });
}
+ Ok(())
+}
+fn failure_envelope(request: &MvpOperationRequest, error: OperationAdapterError) -> OutputEnvelope {
+ OutputEnvelope::failure(
+ request.operation_id(),
+ error.to_output_error(),
+ request
+ .context()
+ .envelope_context(format!("req_{}", request.operation_id().replace('.', "_"))),
+ )
+}
+
+fn render_envelope(
+ envelope: &OutputEnvelope,
+ format: TargetOutputFormat,
+) -> Result<(), runtime::RuntimeError> {
+ let stdout = std::io::stdout();
+ let mut handle = stdout.lock();
+ match format {
+ TargetOutputFormat::Human | TargetOutputFormat::Json => {
+ serde_json::to_writer_pretty(&mut handle, envelope)?;
+ }
+ TargetOutputFormat::Ndjson => {
+ serde_json::to_writer(&mut handle, envelope)?;
+ }
+ }
+ writeln!(handle)?;
Ok(())
}
+
+fn envelope_exit_code(envelope: &OutputEnvelope) -> ExitCode {
+ envelope
+ .errors
+ .first()
+ .map(|error| ExitCode::from(error.exit_code))
+ .unwrap_or_else(|| ExitCode::from(0))
+}
+
+fn operation_config_error(error: OperationAdapterError) -> runtime::RuntimeError {
+ runtime::RuntimeError::Config(error.to_string())
+}
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)]
+
pub mod accounts;
pub mod config;
pub mod daemon;
diff --git a/src/target_cli.rs b/src/target_cli.rs
@@ -12,7 +12,7 @@ pub enum TargetOutputFormat {
}
#[derive(Debug, Parser, Clone)]
-#[command(name = "radroots")]
+#[command(name = "radroots", disable_help_subcommand = true)]
pub struct TargetCliArgs {
#[arg(long = "format", global = true, value_enum, default_value = "human")]
pub format: TargetOutputFormat,
diff --git a/tests/doctor.rs b/tests/doctor.rs
@@ -1,154 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn doctor_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn doctor_reports_unconfigured_local_bootstrap_state() {
- let dir = tempdir().expect("tempdir");
- let output = doctor_command_in(dir.path())
- .args(["--json", "doctor"])
- .output()
- .expect("run doctor");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["ok"], false);
- assert_eq!(json["state"], "warn");
- assert_eq!(json["checks"][0]["name"], "config");
- assert_eq!(json["checks"][0]["status"], "ok");
- assert_eq!(json["checks"][1]["name"], "account");
- assert_eq!(json["checks"][1]["status"], "warn");
- assert_eq!(json["account_resolution"]["source"], "none");
- assert_eq!(json["checks"][2]["name"], "relays");
- assert_eq!(json["checks"][2]["status"], "warn");
- assert_eq!(json["checks"][3]["name"], "signer");
- assert_eq!(json["checks"][3]["status"], "warn");
- assert_eq!(json["checks"][5]["name"], "workflow");
- assert_eq!(json["checks"][5]["status"], "warn");
- assert_eq!(json["source"], "local diagnostics");
- assert_eq!(json["actions"][0], "radroots account new");
- assert_eq!(json["actions"][1], "radroots relay ls");
-}
-
-#[test]
-fn doctor_ignores_cwd_workspace_config_in_interactive_user() {
- let dir = tempdir().expect("tempdir");
- let config_dir = dir.path().join("infra/local/runtime/radroots");
- fs::create_dir_all(&config_dir).expect("workspace config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.cwd\"]\n",
- )
- .expect("write cwd workspace config");
-
- let output = doctor_command_in(dir.path())
- .args(["--json", "doctor"])
- .output()
- .expect("run doctor");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["checks"][0]["name"], "config");
- assert_eq!(json["checks"][0]["detail"], "defaults active");
- assert_eq!(json["checks"][2]["name"], "relays");
- assert_eq!(json["checks"][2]["status"], "warn");
-}
-
-#[test]
-fn doctor_reports_warn_for_ready_local_bootstrap_without_workflow_provider() {
- let dir = tempdir().expect("tempdir");
- let init = doctor_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(init.status.success());
-
- let output = doctor_command_in(dir.path())
- .args(["--json", "--relay", "wss://relay.one", "doctor"])
- .output()
- .expect("run doctor");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["ok"], false);
- assert_eq!(json["state"], "warn");
- assert_eq!(json["checks"][1]["name"], "account");
- assert_eq!(json["checks"][1]["status"], "ok");
- assert_eq!(json["account_resolution"]["source"], "default_account");
- assert_eq!(json["checks"][2]["name"], "relays");
- assert_eq!(json["checks"][2]["status"], "ok");
- assert_eq!(json["checks"][3]["name"], "signer");
- assert_eq!(json["checks"][3]["status"], "ok");
- assert_eq!(json["checks"][5]["name"], "workflow");
- assert_eq!(json["checks"][5]["status"], "warn");
- assert!(json["actions"].is_null());
-}
-
-#[test]
-fn doctor_reports_external_failure_for_missing_myc() {
- let dir = tempdir().expect("tempdir");
- fs::write(
- dir.path().join(".env"),
- "RADROOTS_SIGNER=myc\nRADROOTS_RELAYS=wss://relay.one\n",
- )
- .expect("write env file");
-
- let output = doctor_command_in(dir.path())
- .args(["--json", "--myc-executable", "missing-myc", "doctor"])
- .output()
- .expect("run doctor");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "fail");
- assert_eq!(json["checks"][2]["name"], "relays");
- assert_eq!(json["checks"][2]["status"], "ok");
- assert_eq!(json["checks"][3]["name"], "signer");
- assert_eq!(json["checks"][3]["status"], "fail");
- assert_eq!(json["checks"][4]["name"], "myc");
- assert_eq!(json["checks"][4]["status"], "fail");
- assert_eq!(json["checks"][6]["name"], "workflow");
- assert_eq!(json["checks"][6]["status"], "warn");
- assert_eq!(json["source"], "local diagnostics + myc status command");
-}
diff --git a/tests/farm.rs b/tests/farm.rs
@@ -1,274 +0,0 @@
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn json_output(output: &std::process::Output) -> Value {
- serde_json::from_slice(output.stdout.as_slice()).expect("json output")
-}
-
-fn json_string_array(json: &Value, field: &str) -> Vec<String> {
- json[field]
- .as_array()
- .expect("array field")
- .iter()
- .map(|value| value.as_str().expect("string item").to_owned())
- .collect()
-}
-
-#[test]
-fn farm_init_requires_a_selected_account() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "farm", "init"])
- .output()
- .expect("run farm init");
-
- assert_eq!(output.status.code(), Some(3));
- let json = json_output(&output);
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(
- json["actions"],
- serde_json::json!(["radroots account create"])
- );
-}
-
-#[test]
-fn farm_init_creates_a_minimal_draft_and_reports_missing_fields() {
- let dir = tempdir().expect("tempdir");
-
- let account = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account.status.success());
- let account_json = json_output(&account);
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
-
- let init = cli_command_in(dir.path())
- .args(["--json", "farm", "init"])
- .output()
- .expect("run farm init");
- assert!(init.status.success());
- let init_json = json_output(&init);
- assert_eq!(init_json["state"], "saved");
- assert_eq!(init_json["config"]["selected_account_id"], account_id);
- assert_eq!(
- init_json["actions"],
- serde_json::json!(["radroots farm check"])
- );
-
- let check = cli_command_in(dir.path())
- .args(["farm", "check"])
- .output()
- .expect("run farm check");
- assert_eq!(check.status.code(), Some(3));
- let stdout = String::from_utf8(check.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Farm not ready yet"));
- assert!(stdout.contains("Missing"));
- assert!(stdout.contains("Location"));
- assert!(stdout.contains("Delivery method"));
- assert!(stdout.contains("radroots farm set location \"San Francisco, CA\""));
- assert!(stdout.contains("radroots farm set delivery pickup"));
-
- let publish = cli_command_in(dir.path())
- .args(["--json", "farm", "publish"])
- .output()
- .expect("run farm publish");
- assert_eq!(publish.status.code(), Some(3));
- let publish_json = json_output(&publish);
- assert_eq!(publish_json["state"], "unconfigured");
- let missing = json_string_array(&publish_json, "missing");
- assert!(missing.contains(&"Location".to_owned()));
- assert!(missing.contains(&"Delivery method".to_owned()));
-}
-
-#[test]
-fn farm_set_updates_the_draft_and_farm_check_turns_ready() {
- let dir = tempdir().expect("tempdir");
-
- let account = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account.status.success());
-
- let init = cli_command_in(dir.path())
- .args([
- "farm",
- "init",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--country",
- "US",
- ])
- .output()
- .expect("run farm init");
- assert!(init.status.success());
-
- let set = cli_command_in(dir.path())
- .args(["--json", "farm", "set", "delivery", "pickup"])
- .output()
- .expect("run farm set");
- assert!(set.status.success());
- let set_json = json_output(&set);
- assert_eq!(set_json["state"], "updated");
- assert_eq!(set_json["field"], "Delivery");
- assert_eq!(set_json["value"], "Pickup");
-
- let check = cli_command_in(dir.path())
- .args(["--json", "farm", "check"])
- .output()
- .expect("run farm check");
- assert!(check.status.success());
- let check_json = json_output(&check);
- assert_eq!(check_json["state"], "ready");
- assert_eq!(
- check_json["actions"],
- serde_json::json!(["radroots farm publish"])
- );
- assert!(check_json.get("missing").is_none());
-}
-
-#[test]
-fn farm_show_reports_a_missing_draft() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["farm", "show"])
- .output()
- .expect("run farm show");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Farm draft not found"));
- assert!(stdout.contains("radroots farm init"));
-}
-
-#[test]
-fn farm_setup_compatibility_path_still_produces_a_publishable_draft() {
- let dir = tempdir().expect("tempdir");
-
- let account = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account.status.success());
-
- let setup = cli_command_in(dir.path())
- .args([
- "--json",
- "farm",
- "setup",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ])
- .output()
- .expect("run farm setup");
- assert!(setup.status.success());
- let setup_json = json_output(&setup);
- assert_eq!(setup_json["state"], "configured");
- assert_eq!(
- setup_json["actions"],
- serde_json::json!(["radroots farm check", "radroots farm publish"])
- );
-
- let check = cli_command_in(dir.path())
- .args(["--json", "farm", "check"])
- .output()
- .expect("run farm check");
- assert!(check.status.success());
- let check_json = json_output(&check);
- assert_eq!(check_json["state"], "ready");
-}
-
-#[test]
-fn listing_new_points_back_to_farm_check_when_defaults_are_incomplete() {
- let dir = tempdir().expect("tempdir");
-
- let account = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account.status.success());
-
- let init = cli_command_in(dir.path())
- .args(["farm", "init", "--name", "La Huerta"])
- .output()
- .expect("run farm init");
- assert!(init.status.success());
-
- let listing = cli_command_in(dir.path())
- .args([
- "--json",
- "listing",
- "new",
- "--key",
- "eggs",
- "--title",
- "Pasture eggs",
- "--category",
- "protein",
- "--summary",
- "Fresh pasture-raised eggs.",
- ])
- .output()
- .expect("run listing new");
- assert!(listing.status.success());
- let listing_json = json_output(&listing);
- assert_eq!(
- listing_json["reason"],
- "selected farm draft is missing delivery or location defaults; those fields were left blank"
- );
- let actions = json_string_array(&listing_json, "actions");
- assert!(actions.iter().any(|action| action == "radroots farm check"));
-}
diff --git a/tests/find.rs b/tests/find.rs
@@ -1,541 +0,0 @@
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use radroots_sql_core::{SqlExecutor, SqliteExecutor};
-use serde_json::{Value, json};
-use tempfile::tempdir;
-
-const ADDRESS_BACKED_LISTING_ADDR: &str =
- "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_HYF_ENABLED",
- "RADROOTS_HYF_EXECUTABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn find_reports_unconfigured_when_local_replica_is_missing() {
- let dir = tempdir().expect("tempdir");
- let output = cli_command_in(dir.path())
- .args(["--json", "find", "eggs"])
- .output()
- .expect("run find");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["actions"][0], "radroots local init");
-}
-
-#[test]
-fn find_returns_json_and_ndjson_from_local_market_rows() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000101",
- "heirloom-tomato",
- Some(ADDRESS_BACKED_LISTING_ADDR),
- "produce",
- "Heirloom Tomato",
- "Bright red slicing tomatoes",
- 18,
- 12,
- Some("Asheville"),
- );
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000102",
- "tomato-sauce",
- None,
- "prepared",
- "Tomato Sauce",
- "Slow cooked tomato sauce",
- 8,
- 6,
- Some("Black Mountain"),
- );
-
- let json_output = cli_command_in(dir.path())
- .args(["--json", "find", "tomato"])
- .output()
- .expect("run json find");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["count"], 2);
- assert_eq!(
- json["results"][0]["provenance"]["origin"],
- "local_replica.trade_product"
- );
- assert_eq!(json["results"][0]["location_primary"], "Asheville");
- assert_eq!(
- json["results"][0]["listing_addr"],
- ADDRESS_BACKED_LISTING_ADDR
- );
-
- let ndjson_output = cli_command_in(dir.path())
- .args(["--ndjson", "find", "tomato"])
- .output()
- .expect("run ndjson find");
- assert!(ndjson_output.status.success());
- let stdout = String::from_utf8(ndjson_output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines[0].contains("\"title\":\"Heirloom Tomato\""));
- assert!(lines[0].contains("\"listing_addr\""));
- assert!(lines[1].contains("\"title\":\"Tomato Sauce\""));
-}
-
-#[test]
-fn find_human_output_uses_market_cards_without_internal_footer() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000103",
- "fresh-eggs",
- None,
- "protein",
- "Fresh Eggs",
- "Pasture-raised eggs",
- 36,
- 24,
- Some("Marshall"),
- );
-
- let output = cli_command_in(dir.path())
- .args(["find", "eggs"])
- .output()
- .expect("run human find");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("1 listing for eggs"));
- assert!(stdout.contains("Fresh Eggs"));
- assert!(stdout.contains("Key"));
- assert!(stdout.contains("Price"));
- assert!(!stdout.contains("provenance:"));
- assert!(!stdout.contains("source:"));
-}
-
-#[test]
-fn find_reports_empty_results_without_failing() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "find", "saffron"])
- .output()
- .expect("run empty find");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "empty");
- assert_eq!(json["count"], 0);
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|reason| reason.contains("no local market results matched"))
- );
-}
-
-#[test]
-fn find_uses_hyf_query_rewrite_when_available() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000104",
- "fresh-eggs",
- None,
- "protein",
- "Fresh Eggs",
- "Pasture-raised eggs",
- 36,
- 24,
- Some("Marshall"),
- );
-
- let hyfd = write_fake_hyfd(
- dir.path(),
- r#"{"version":1,"request_id":"cli-doctor-hyf-status","trace_id":"cli-doctor-hyf-status","ok":true,"output":{"build_identity":{"protocol_version":1},"enabled_execution_modes":{"deterministic":true}}}"#,
- r#"{"version":1,"request_id":"cli-find-query-rewrite","trace_id":"cli-find-query-rewrite","ok":true,"output":{"original_text":"henhouse","normalized_text":"henhouse","rewritten_text":"eggs","query_terms":["eggs"],"normalization_signals":["query_rewrite"],"ranking_hints":["local_first"],"extracted_filters":{"local_intent":false,"fulfillment":"any","time_window":"any"}}}"#,
- );
-
- let json_output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", &hyfd)
- .args(["--json", "find", "henhouse"])
- .output()
- .expect("run hyf json find");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["count"], 1);
- assert_eq!(json["query"], "henhouse");
- assert_eq!(json["hyf"]["state"], "query_rewrite_applied");
- assert_eq!(json["hyf"]["rewritten_query"], "eggs");
- assert_eq!(json["hyf"]["query_terms"], json!(["eggs"]));
- assert_eq!(json["results"][0]["title"], "Fresh Eggs");
- assert_eq!(json["results"][0]["hyf"]["rewritten_query"], "eggs");
-
- let human_output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", &hyfd)
- .args(["find", "henhouse"])
- .output()
- .expect("run hyf human find");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("1 listing for eggs"));
- assert!(stdout.contains("Also searched for"));
- assert!(stdout.contains("henhouse"));
-
- let ndjson_output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", &hyfd)
- .args(["--ndjson", "find", "henhouse"])
- .output()
- .expect("run hyf ndjson find");
- assert!(ndjson_output.status.success());
- let stdout = String::from_utf8(ndjson_output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 1);
- assert!(lines[0].contains("\"title\":\"Fresh Eggs\""));
- assert!(lines[0].contains("\"rewritten_query\":\"eggs\""));
-}
-
-#[test]
-fn find_human_output_tiers_change_information_budget() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000105",
- "fresh-eggs",
- None,
- "protein",
- "Fresh Eggs",
- "Pasture-raised eggs",
- 36,
- 24,
- Some("Marshall"),
- );
-
- let quiet_output = cli_command_in(dir.path())
- .args(["--quiet", "find", "eggs"])
- .output()
- .expect("run quiet find");
- assert!(quiet_output.status.success());
- let quiet_stdout = String::from_utf8(quiet_output.stdout).expect("utf8 stdout");
- assert_eq!(quiet_stdout.trim(), "fresh-eggs");
-
- let default_output = cli_command_in(dir.path())
- .args(["find", "eggs"])
- .output()
- .expect("run default find");
- assert!(default_output.status.success());
- let default_stdout = String::from_utf8(default_output.stdout).expect("utf8 stdout");
- assert!(default_stdout.contains("1 listing for eggs"));
- assert!(!default_stdout.contains("Details"));
- assert!(!default_stdout.contains("Trace"));
- assert!(!default_stdout.contains("Source"));
-
- let verbose_output = cli_command_in(dir.path())
- .args(["--verbose", "find", "eggs"])
- .output()
- .expect("run verbose find");
- assert!(verbose_output.status.success());
- let verbose_stdout = String::from_utf8(verbose_output.stdout).expect("utf8 stdout");
- assert!(verbose_stdout.contains("1 listing for eggs"));
- assert!(verbose_stdout.contains("Details"));
- assert!(verbose_stdout.contains("Source"));
- assert!(verbose_stdout.contains("Freshness"));
- assert!(verbose_stdout.contains("Relay count"));
- assert!(!verbose_stdout.contains("Trace"));
-
- let trace_output = cli_command_in(dir.path())
- .args(["--trace", "find", "eggs"])
- .output()
- .expect("run trace find");
- assert!(trace_output.status.success());
- let trace_stdout = String::from_utf8(trace_output.stdout).expect("utf8 stdout");
- assert!(trace_stdout.contains("Details"));
- assert!(trace_stdout.contains("Trace"));
- assert!(trace_stdout.contains("Command"));
- assert!(trace_stdout.contains("\"source\""));
-}
-
-#[test]
-fn find_uses_hyf_query_rewrite_without_status_preflight() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000106",
- "fresh-eggs",
- None,
- "protein",
- "Fresh Eggs",
- "Pasture-raised eggs",
- 36,
- 24,
- Some("Marshall"),
- );
-
- let hyfd = write_fake_hyfd_with_failing_status(
- dir.path(),
- r#"{"version":1,"request_id":"cli-find-query-rewrite","trace_id":"cli-find-query-rewrite","ok":true,"output":{"original_text":"henhouse","normalized_text":"henhouse","rewritten_text":"eggs","query_terms":["eggs"],"normalization_signals":["query_rewrite"],"ranking_hints":["local_first"],"extracted_filters":{"local_intent":false,"fulfillment":"any","time_window":"any"}}}"#,
- );
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", &hyfd)
- .args(["--json", "find", "henhouse"])
- .output()
- .expect("run hyf json find");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["count"], 1);
- assert_eq!(json["hyf"]["state"], "query_rewrite_applied");
- assert_eq!(json["hyf"]["rewritten_query"], "eggs");
- assert_eq!(json["results"][0]["title"], "Fresh Eggs");
-}
-
-#[test]
-fn find_falls_back_cleanly_when_hyf_is_unavailable() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000105",
- "fresh-eggs",
- None,
- "protein",
- "Fresh Eggs",
- "Pasture-raised eggs",
- 36,
- 24,
- Some("Marshall"),
- );
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", dir.path().join("missing-hyfd"))
- .args(["--json", "find", "eggs"])
- .output()
- .expect("run fallback find");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["count"], 1);
- assert!(json["hyf"].is_null());
- assert_eq!(json["results"][0]["title"], "Fresh Eggs");
-}
-
-fn seed_trade_product(
- workdir: &Path,
- product_id: &str,
- key: &str,
- listing_addr: Option<&str>,
- category: &str,
- title: &str,
- summary: &str,
- qty_amt: i64,
- qty_avail: i64,
- location_label: Option<&str>,
-) {
- let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
- let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
- let now = "2026-04-07T00:00:00.000Z";
- executor
- .exec(
- "INSERT INTO trade_product (id, created_at, updated_at, key, listing_addr, 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- product_id,
- now,
- now,
- key,
- listing_addr,
- category,
- title,
- summary,
- "fresh",
- "lot-a",
- "standard",
- 2026,
- qty_amt,
- "kg",
- "kg",
- qty_avail,
- 12.5,
- "USD",
- 1,
- "kg",
- Value::Null
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert trade product");
-
- if let Some(location_label) = location_label {
- let location_id = format!("11111111-1111-1111-1111-{}", &product_id[24..]);
- executor
- .exec(
- "INSERT INTO gcs_location (id, created_at, updated_at, d_tag, lat, lng, geohash, point, polygon, accuracy, altitude, tag_0, label, area, elevation, soil, climate, gc_id, gc_name, gc_admin1_id, gc_admin1_name, gc_country_id, gc_country_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- location_id,
- now,
- now,
- format!("location-{product_id}"),
- 35.0,
- -82.0,
- "dnrj",
- "POINT(-82 35)",
- "POLYGON EMPTY",
- Value::Null,
- Value::Null,
- Value::Null,
- location_label,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- location_label,
- Value::Null,
- Value::Null,
- Value::Null,
- "USA"
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert gcs location");
- executor
- .exec(
- "INSERT INTO trade_product_location (tb_tp, tb_gl) VALUES (?, ?);",
- json!([product_id, location_id]).to_string().as_str(),
- )
- .expect("insert trade product location");
- }
-}
-
-fn write_fake_hyfd(
- workdir: &Path,
- status_response: &str,
- rewrite_response: &str,
-) -> std::path::PathBuf {
- let path = workdir.join("fake-hyfd");
- let script = format!(
- "#!/bin/sh\nread -r request || exit 64\ncase \"$request\" in\n *'\"capability\":\"sys.status\"'*)\n cat <<'JSON'\n{status_response}\nJSON\n ;;\n *'\"capability\":\"query_rewrite\"'*)\n cat <<'JSON'\n{rewrite_response}\nJSON\n ;;\n *)\n cat <<'JSON'\n{{\"version\":1,\"request_id\":\"unexpected\",\"ok\":false,\"error\":{{\"code\":\"unsupported_capability\",\"message\":\"unexpected request\"}}}}\nJSON\n ;;\nesac\n"
- );
- std::fs::write(&path, script).expect("write fake hyfd");
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let mut permissions = std::fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- std::fs::set_permissions(&path, permissions).expect("chmod fake hyfd");
- }
- path
-}
-
-fn write_fake_hyfd_with_failing_status(
- workdir: &Path,
- rewrite_response: &str,
-) -> std::path::PathBuf {
- let path = workdir.join("fake-hyfd");
- let script = format!(
- "#!/bin/sh\nread -r request || exit 64\ncase \"$request\" in\n *'\"capability\":\"sys.status\"'*)\n echo \"status should not be called\" >&2\n exit 23\n ;;\n *'\"capability\":\"query_rewrite\"'*)\n cat <<'JSON'\n{rewrite_response}\nJSON\n ;;\n *)\n cat <<'JSON'\n{{\"version\":1,\"request_id\":\"unexpected\",\"ok\":false,\"error\":{{\"code\":\"unsupported_capability\",\"message\":\"unexpected request\"}}}}\nJSON\n ;;\nesac\n"
- );
- std::fs::write(&path, script).expect("write fake hyfd");
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let mut permissions = std::fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- std::fs::set_permissions(&path, permissions).expect("chmod fake hyfd");
- }
- path
-}
diff --git a/tests/help.rs b/tests/help.rs
@@ -1,168 +0,0 @@
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-
-fn help_command() -> Command {
- Command::cargo_bin("radroots").expect("binary")
-}
-
-#[test]
-fn root_help_is_workflow_grouped() {
- let output = help_command()
- .arg("--help")
- .output()
- .expect("run root help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Start here"));
- assert!(stdout.contains("Sell from your farm"));
- assert!(stdout.contains("Buy from the market"));
- assert!(stdout.contains("Accounts and settings"));
- assert!(stdout.contains("Advanced and troubleshooting"));
- assert!(stdout.contains("setup"));
- assert!(stdout.contains("status"));
- assert!(stdout.contains("market"));
- assert!(stdout.contains("sell"));
- assert!(stdout.contains("Examples"));
-}
-
-#[test]
-fn setup_help_describes_the_landed_workflow_layer() {
- let output = help_command()
- .args(["setup", "--help"])
- .output()
- .expect("run setup help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains(
- "This workflow layer sits on top of the existing account, local, and farm commands."
- ));
- assert!(stdout.contains(
- "Use `radroots account create` or `radroots account select` explicitly when no actor is resolved."
- ));
- assert!(!stdout.contains("being added"));
-}
-
-#[test]
-fn status_help_describes_current_readiness_surfaces() {
- let output = help_command()
- .args(["status", "--help"])
- .output()
- .expect("run status help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains(
- "This workflow summary reflects the current readiness and configuration surfaces."
- ));
- assert!(stdout.contains(
- "When no actor is resolved, it points to explicit account commands instead of mutating account state."
- ));
- assert!(!stdout.contains("being added"));
-}
-
-#[test]
-fn account_help_prefers_human_first_aliases() {
- let output = help_command()
- .args(["account", "--help"])
- .output()
- .expect("run account help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("create"));
- assert!(stdout.contains("import"));
- assert!(stdout.contains("view"));
- assert!(stdout.contains("list"));
- assert!(stdout.contains("select"));
- assert!(stdout.contains("clear-default"));
- assert!(stdout.contains("remove"));
- assert!(stdout.contains("Select stores the default account."));
- assert!(stdout.contains("Compatibility aliases: new, whoami, ls, use."));
-}
-
-#[test]
-fn farm_help_mentions_human_first_subcommands() {
- let output = help_command()
- .args(["farm", "--help"])
- .output()
- .expect("run farm help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("init"));
- assert!(stdout.contains("set"));
- assert!(stdout.contains("check"));
- assert!(stdout.contains("show"));
- assert!(stdout.contains("publish"));
- assert!(stdout.contains(
- "Compatibility paths: `farm setup`, `farm status`, and `farm get` remain available."
- ));
-}
-
-#[test]
-fn market_help_is_example_first() {
- let output = help_command()
- .args(["market", "--help"])
- .output()
- .expect("run market help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("update"));
- assert!(stdout.contains("search"));
- assert!(stdout.contains("view"));
- assert!(stdout.contains("radroots market search tomatoes"));
- assert!(
- stdout.contains(
- "Compatibility paths: `sync pull`, `find`, and `listing get` remain available."
- )
- );
-}
-
-#[test]
-fn sell_help_mentions_listing_compatibility() {
- let output = help_command()
- .args(["sell", "--help"])
- .output()
- .expect("run sell help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("add"));
- assert!(stdout.contains("show"));
- assert!(stdout.contains("check"));
- assert!(stdout.contains("publish"));
- assert!(stdout.contains("update"));
- assert!(stdout.contains("pause"));
- assert!(stdout.contains("reprice"));
- assert!(stdout.contains("restock"));
- assert!(
- stdout.contains(
- "Compatibility path: the advanced `listing` command family remains available."
- )
- );
-}
-
-#[test]
-fn order_help_prefers_create_view_and_list() {
- let output = help_command()
- .args(["order", "--help"])
- .output()
- .expect("run order help");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("create"));
- assert!(stdout.contains("view"));
- assert!(stdout.contains("list"));
- assert!(stdout.contains("submit"));
- assert!(stdout.contains("watch"));
- assert!(stdout.contains("cancel"));
- assert!(stdout.contains("history"));
- assert!(stdout.contains("radroots order submit ord_demo --watch"));
- assert!(stdout.contains("Compatibility aliases: new, get, ls."));
- assert!(stdout.contains("durable order cancel availability"));
-}
diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs
@@ -1,601 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use radroots_identity::RadrootsIdentity;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn secrets_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("roaming").join("Radroots").join("secrets")
- } else {
- workdir.join("home").join(".radroots").join("secrets")
- }
-}
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn account_new_json_creates_local_account_store_entry() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
-
- assert!(output.status.success());
- assert!(store_path.exists());
-
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "created");
- assert_eq!(json["source"], "shared account store ยท local first");
- assert!(json["account"]["id"].is_string());
- assert_eq!(json["account"]["signer"], "local");
- assert_eq!(json["account"]["is_default"], true);
- assert!(json["public_identity"]["id"].is_string());
- assert!(json["public_identity"]["public_key_hex"].is_string());
- assert!(json["public_identity"]["public_key_npub"].is_string());
-}
-
-#[test]
-fn account_create_quiet_reports_created_account_id() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["--quiet", "account", "create"])
- .output()
- .expect("run quiet account create");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let line = stdout.trim();
- assert!(line.starts_with("Account created: "));
- assert!(line.len() > "Account created: ".len());
-}
-
-#[test]
-fn account_new_encrypts_file_backed_secret_fallback_by_default() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- let account_id = json["account"]["id"].as_str().expect("account id");
- let secrets_dir = secrets_root(dir.path()).join("shared/accounts");
- let envelope_path = secrets_dir.join(format!("{account_id}.secret.json"));
-
- assert!(secrets_dir.join(".vault.key").exists());
- assert!(envelope_path.exists());
- assert!(!secrets_dir.join(format!("{account_id}.secret")).exists());
-
- let envelope: Value = serde_json::from_slice(
- fs::read(envelope_path)
- .expect("read encrypted envelope")
- .as_slice(),
- )
- .expect("envelope json");
- assert_eq!(envelope["header"]["cipher"], "x_cha_cha20_poly1305");
- assert_eq!(envelope["header"]["key_source"], "secret_vault_wrapped");
- assert!(envelope["ciphertext"].is_array());
-}
-
-#[test]
-fn account_import_json_creates_watch_only_account_without_secret_material() {
- let dir = tempdir().expect("tempdir");
- let identity = RadrootsIdentity::generate();
- let import_path = dir.path().join("watch-only-identity.json");
- fs::write(
- &import_path,
- serde_json::to_vec_pretty(&identity.to_public()).expect("serialize public identity"),
- )
- .expect("write import file");
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "account",
- "import",
- import_path.to_str().expect("utf8 path"),
- ])
- .output()
- .expect("run account import");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- let account_id = json["account"]["id"].as_str().expect("account id");
- assert_eq!(json["state"], "imported");
- assert_eq!(json["source"], "shared account store ยท watch-only import");
- assert_eq!(json["account"]["signer"], "watch_only");
- assert_eq!(json["account"]["is_default"], true);
-
- let secrets_dir = secrets_root(dir.path()).join("shared/accounts");
- assert!(!secrets_dir.join(format!("{account_id}.secret")).exists());
- assert!(
- !secrets_dir
- .join(format!("{account_id}.secret.json"))
- .exists()
- );
-
- let whoami = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
- assert!(whoami.status.success());
- let whoami_json: Value = serde_json::from_slice(whoami.stdout.as_slice()).expect("whoami json");
- assert_eq!(
- whoami_json["account_resolution"]["resolved_account"]["id"],
- account_id
- );
- assert_eq!(
- whoami_json["account_resolution"]["resolved_account"]["signer"],
- "watch_only"
- );
-}
-
-#[test]
-fn account_new_rejects_dry_run_without_creating_store_state() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let output = cli_command_in(dir.path())
- .args(["--dry-run", "account", "new"])
- .output()
- .expect("run account new");
-
- assert_eq!(output.status.code(), Some(2));
- assert!(!store_path.exists());
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("`account create` does not support --dry-run yet"));
-}
-
-#[test]
-fn account_new_rejects_plaintext_fallback_backend() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_ACCOUNT_SECRET_BACKEND", "host_vault")
- .env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "plaintext_file")
- .args(["account", "new"])
- .output()
- .expect("run account new");
-
- assert_eq!(output.status.code(), Some(2));
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("must be `host_vault`, `encrypted_file`, or `none`"));
-}
-
-#[test]
-fn account_new_rejects_memory_secret_backend_without_creating_store_state() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_ACCOUNT_SECRET_BACKEND", "memory")
- .args(["account", "new"])
- .output()
- .expect("run account new");
-
- assert_eq!(output.status.code(), Some(2));
- assert!(!store_path.exists());
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("RADROOTS_ACCOUNT_SECRET_BACKEND"));
- assert!(stderr.contains("must be `host_vault` or `encrypted_file`"));
-}
-
-#[test]
-fn account_new_rejects_memory_secret_fallback_without_creating_store_state() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_ACCOUNT_SECRET_BACKEND", "host_vault")
- .env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "memory")
- .args(["account", "new"])
- .output()
- .expect("run account new");
-
- assert_eq!(output.status.code(), Some(2));
- assert!(!store_path.exists());
- assert!(output.stdout.is_empty());
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("RADROOTS_ACCOUNT_SECRET_FALLBACK"));
- assert!(stderr.contains("must be `host_vault`, `encrypted_file`, or `none`"));
-}
-
-#[test]
-fn account_whoami_json_reads_default_account() {
- let dir = tempdir().expect("tempdir");
-
- let init = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(init.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["source"], "shared account store ยท local first");
- assert_eq!(json["account_resolution"]["source"], "default_account");
- assert!(json["account_resolution"]["resolved_account"]["id"].is_string());
- assert_eq!(
- json["account_resolution"]["resolved_account"]["signer"],
- "local"
- );
- assert_eq!(
- json["account_resolution"]["resolved_account"]["is_default"],
- true
- );
- assert_eq!(
- json["account_resolution"]["default_account"]["id"],
- json["account_resolution"]["resolved_account"]["id"]
- );
- assert!(json["public_identity"]["id"].is_string());
-}
-
-#[test]
-fn account_new_does_not_replace_existing_default_account() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
- let first_json: Value = serde_json::from_slice(first.stdout.as_slice()).expect("first json");
- let first_id = first_json["account"]["id"]
- .as_str()
- .expect("first account id")
- .to_owned();
- assert_eq!(first_json["account"]["is_default"], true);
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
- let second_json: Value = serde_json::from_slice(second.stdout.as_slice()).expect("second json");
- assert_eq!(second_json["account"]["is_default"], false);
-
- let store_json: Value =
- serde_json::from_slice(fs::read(&store_path).expect("read store").as_slice())
- .expect("parse store");
- assert_eq!(store_json["default_account_id"], first_id);
-
- let whoami = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
- assert!(whoami.status.success());
- let whoami_json: Value = serde_json::from_slice(whoami.stdout.as_slice()).expect("whoami json");
- assert_eq!(
- whoami_json["account_resolution"]["resolved_account"]["id"],
- first_id
- );
-}
-
-#[test]
-fn account_clear_default_json_clears_stored_default_without_removing_accounts() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
- let first_json: Value = serde_json::from_slice(first.stdout.as_slice()).expect("first json");
- let first_id = first_json["account"]["id"]
- .as_str()
- .expect("first account id")
- .to_owned();
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "account", "clear-default"])
- .output()
- .expect("run clear-default");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("clear-default json");
- assert_eq!(json["state"], "cleared");
- assert_eq!(json["cleared_account"]["id"], first_id);
- assert_eq!(json["remaining_account_count"], 2);
-
- let store_json: Value =
- serde_json::from_slice(fs::read(&store_path).expect("read store").as_slice())
- .expect("parse store");
- assert_eq!(store_json["default_account_id"], Value::Null);
- assert_eq!(
- store_json["accounts"]
- .as_array()
- .expect("accounts array")
- .len(),
- 2
- );
-
- let whoami = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
- assert_eq!(whoami.status.code(), Some(3));
- let whoami_json: Value = serde_json::from_slice(whoami.stdout.as_slice()).expect("whoami json");
- assert_eq!(whoami_json["account_resolution"]["source"], "none");
- assert_eq!(
- whoami_json["account_resolution"]["default_account"],
- Value::Null
- );
-}
-
-#[test]
-fn account_whoami_json_reports_unconfigured_without_accounts() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
-
- assert_eq!(output.status.code(), Some(3));
-
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["account_resolution"]["source"], "none");
- assert_eq!(json["account_resolution"]["resolved_account"], Value::Null);
- assert_eq!(json["account_resolution"]["default_account"], Value::Null);
- assert_eq!(json["public_identity"], Value::Null);
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("no local accounts found"))
- );
-}
-
-#[test]
-fn account_ls_ndjson_emits_one_line_per_account() {
- let dir = tempdir().expect("tempdir");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--ndjson", "account", "ls"])
- .output()
- .expect("run account ls");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines[0].contains("\"id\":"));
- assert!(lines[1].contains("\"id\":"));
-}
-
-#[test]
-fn account_use_selects_existing_account() {
- let dir = tempdir().expect("tempdir");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
- let first_json: Value =
- serde_json::from_slice(first.stdout.as_slice()).expect("first account json");
- let first_id = first_json["account"]["id"]
- .as_str()
- .expect("first account id")
- .to_owned();
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
-
- let use_output = cli_command_in(dir.path())
- .args(["--json", "account", "use", first_id.as_str()])
- .output()
- .expect("run account use");
-
- assert!(use_output.status.success());
- let use_json: Value =
- serde_json::from_slice(use_output.stdout.as_slice()).expect("account use json");
- assert_eq!(use_json["state"], "default");
- assert_eq!(use_json["default_account_id"], first_id);
- assert_eq!(use_json["account"]["is_default"], true);
-
- let whoami = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
- assert!(whoami.status.success());
- let whoami_json: Value =
- serde_json::from_slice(whoami.stdout.as_slice()).expect("account whoami json");
- assert_eq!(
- whoami_json["account_resolution"]["source"],
- "default_account"
- );
- assert_eq!(
- whoami_json["account_resolution"]["resolved_account"]["id"],
- first_id
- );
- assert_eq!(
- whoami_json["account_resolution"]["resolved_account"]["is_default"],
- true
- );
-}
-
-#[test]
-fn account_remove_json_clears_default_when_removing_default_account() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
- let first_json: Value = serde_json::from_slice(first.stdout.as_slice()).expect("first json");
- let first_id = first_json["account"]["id"]
- .as_str()
- .expect("first account id")
- .to_owned();
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "account", "remove", first_id.as_str()])
- .output()
- .expect("run account remove");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("remove json");
- assert_eq!(json["state"], "removed");
- assert_eq!(json["removed_account"]["id"], first_id);
- assert_eq!(json["default_cleared"], true);
- assert_eq!(json["remaining_account_count"], 1);
-
- let store_json: Value =
- serde_json::from_slice(fs::read(&store_path).expect("read store").as_slice())
- .expect("parse store");
- assert_eq!(store_json["default_account_id"], Value::Null);
- assert_eq!(
- store_json["accounts"]
- .as_array()
- .expect("accounts array")
- .len(),
- 1
- );
-
- let whoami = cli_command_in(dir.path())
- .args(["--json", "account", "whoami"])
- .output()
- .expect("run account whoami");
- assert_eq!(whoami.status.code(), Some(3));
- let whoami_json: Value = serde_json::from_slice(whoami.stdout.as_slice()).expect("whoami json");
- assert_eq!(whoami_json["account_resolution"]["source"], "none");
- assert_eq!(
- whoami_json["account_resolution"]["default_account"],
- Value::Null
- );
-}
-
-#[test]
-fn account_use_rejects_ambiguous_label_selector() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
-
- let mut store_json: Value =
- serde_json::from_slice(fs::read(&store_path).expect("read store").as_slice())
- .expect("parse store");
- let accounts = store_json["accounts"]
- .as_array_mut()
- .expect("accounts array");
- accounts[0]["label"] = Value::from("shared");
- accounts[1]["label"] = Value::from("shared");
- fs::write(
- &store_path,
- serde_json::to_vec_pretty(&store_json).expect("serialize store"),
- )
- .expect("write store");
-
- let output = cli_command_in(dir.path())
- .args(["account", "use", "shared"])
- .output()
- .expect("run account use");
-
- assert_eq!(output.status.code(), Some(2));
- let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("matched multiple local accounts"));
-}
diff --git a/tests/job_rpc.rs b/tests/job_rpc.rs
@@ -1,888 +0,0 @@
-use std::fs;
-use std::io::{Read, Write};
-use std::net::{TcpListener, TcpStream};
-use std::path::Path;
-use std::process::Command;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
-use std::thread::{self, JoinHandle};
-use std::time::Duration;
-
-use assert_cmd::prelude::*;
-use serde_json::{Value, json};
-use tempfile::tempdir;
-
-fn job_rpc_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn job_rpc_test_guard() -> MutexGuard<'static, ()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(()))
- .lock()
- .expect("job rpc test lock")
-}
-
-fn write_user_config(workdir: &Path, contents: &str) {
- let config_dir = workdir.join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(config_dir.join("config.toml"), contents).expect("write user config");
-}
-
-fn signer_session_config(url: &str) -> String {
- format!(
- r#"
-[[capability_binding]]
-capability = "write_plane.trade_jsonrpc"
-provider = "radrootsd"
-target_kind = "explicit_endpoint"
-target = "{url}"
-
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "acct_user"
-signer_session_ref = "myc_conn_1"
-"#
- )
-}
-
-fn run_signer_session_command<F>(
- workdir: &Path,
- args: &[&str],
- handler: F,
-) -> (Value, MockRpcRequest)
-where
- F: Fn(&MockRpcRequest) -> MockRpcResponse + Send + Sync + 'static,
-{
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |request| {
- let response = handler(&request);
- recorded.lock().expect("record requests").push(request);
- response
- });
- write_user_config(
- workdir,
- signer_session_config(server.url().as_str()).as_str(),
- );
- let output = job_rpc_command_in(workdir)
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args(args)
- .output()
- .expect("run signer session command");
- assert!(
- output.status.success(),
- "command {:?} stdout: {}\nstderr: {}",
- args,
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- );
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- let request = requests
- .lock()
- .expect("recorded requests")
- .first()
- .expect("one recorded request")
- .clone();
- (json, request)
-}
-
-#[derive(Debug, Clone)]
-struct MockRpcRequest {
- method: String,
- auth_header: Option<String>,
- body: Value,
-}
-
-#[derive(Debug, Clone)]
-struct MockRpcResponse {
- status_code: u16,
- body: Value,
-}
-
-impl MockRpcResponse {
- fn success(result: Value) -> Self {
- Self {
- status_code: 200,
- body: json!({
- "jsonrpc": "2.0",
- "id": 1,
- "result": result,
- }),
- }
- }
-
- fn rpc_error(code: i64, message: &str) -> Self {
- Self {
- status_code: 200,
- body: json!({
- "jsonrpc": "2.0",
- "id": 1,
- "error": {
- "code": code,
- "message": message,
- }
- }),
- }
- }
-}
-
-struct MockRpcServer {
- address: String,
- shutdown: Arc<AtomicBool>,
- handle: Option<JoinHandle<()>>,
-}
-
-impl MockRpcServer {
- fn start<F>(handler: F) -> Self
- where
- F: Fn(MockRpcRequest) -> MockRpcResponse + Send + Sync + 'static,
- {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock rpc listener");
- listener
- .set_nonblocking(true)
- .expect("set mock rpc listener nonblocking");
- let address = listener
- .local_addr()
- .expect("mock rpc local addr")
- .to_string();
- let shutdown = Arc::new(AtomicBool::new(false));
- let shutdown_flag = Arc::clone(&shutdown);
- let handler: Arc<dyn Fn(MockRpcRequest) -> MockRpcResponse + Send + Sync> =
- Arc::new(handler);
- let handle = thread::spawn(move || {
- while !shutdown_flag.load(Ordering::SeqCst) {
- match listener.accept() {
- Ok((mut stream, _)) => {
- let _ = stream.set_nonblocking(false);
- match read_request(&mut stream) {
- Ok(request) => {
- let response = handler(request);
- let _ = write_response(&mut stream, &response);
- }
- Err(_) => {}
- }
- }
- Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
- thread::sleep(Duration::from_millis(10));
- }
- Err(_) => {
- thread::sleep(Duration::from_millis(10));
- }
- }
- }
- });
-
- Self {
- address,
- shutdown,
- handle: Some(handle),
- }
- }
-
- fn url(&self) -> String {
- format!("http://{}", self.address)
- }
-}
-
-impl Drop for MockRpcServer {
- fn drop(&mut self) {
- self.shutdown.store(true, Ordering::SeqCst);
- let _ = TcpStream::connect(&self.address);
- if let Some(handle) = self.handle.take() {
- handle.join().expect("join mock rpc server thread");
- }
- }
-}
-
-fn read_request(stream: &mut TcpStream) -> Result<MockRpcRequest, String> {
- stream
- .set_read_timeout(Some(Duration::from_secs(2)))
- .map_err(|error| format!("set mock rpc read timeout: {error}"))?;
-
- let mut buffer = Vec::new();
- let mut chunk = [0_u8; 4096];
- let mut header_end = None;
- let mut content_length = 0_usize;
-
- loop {
- let read = stream
- .read(&mut chunk)
- .map_err(|error| format!("read mock rpc request: {error}"))?;
- if read == 0 {
- break;
- }
- buffer.extend_from_slice(&chunk[..read]);
- if header_end.is_none() {
- header_end = find_subslice(&buffer, b"\r\n\r\n").map(|index| index + 4);
- if let Some(end) = header_end {
- content_length = parse_content_length(&buffer[..end])?;
- if buffer.len() >= end + content_length {
- break;
- }
- }
- } else if let Some(end) = header_end {
- if buffer.len() >= end + content_length {
- break;
- }
- }
- }
-
- let end = header_end.ok_or_else(|| "mock rpc request did not include headers".to_owned())?;
- let headers = std::str::from_utf8(&buffer[..end])
- .map_err(|error| format!("mock rpc headers were not utf-8: {error}"))?;
- let auth_header = parse_header(headers, "authorization");
- let body = std::str::from_utf8(&buffer[end..end + content_length])
- .map_err(|error| format!("mock rpc body was not utf-8: {error}"))?;
- let envelope: Value =
- serde_json::from_str(body).map_err(|error| format!("parse mock rpc json body: {error}"))?;
- let method = envelope["method"]
- .as_str()
- .ok_or_else(|| "mock rpc body did not include method".to_owned())?
- .to_owned();
-
- Ok(MockRpcRequest {
- method,
- auth_header,
- body: envelope,
- })
-}
-
-fn parse_content_length(headers: &[u8]) -> Result<usize, String> {
- let text = std::str::from_utf8(headers)
- .map_err(|error| format!("mock rpc header parse failed: {error}"))?;
- for line in text.lines() {
- if let Some((name, value)) = line.split_once(':') {
- if name.trim().eq_ignore_ascii_case("content-length") {
- return value
- .trim()
- .parse::<usize>()
- .map_err(|error| format!("mock rpc content-length parse failed: {error}"));
- }
- }
- }
- Ok(0)
-}
-
-fn parse_header(headers: &str, wanted: &str) -> Option<String> {
- headers.lines().find_map(|line| {
- let (name, value) = line.split_once(':')?;
- if name.trim().eq_ignore_ascii_case(wanted) {
- Some(value.trim().to_owned())
- } else {
- None
- }
- })
-}
-
-fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
- haystack
- .windows(needle.len())
- .position(|window| window == needle)
-}
-
-fn write_response(stream: &mut TcpStream, response: &MockRpcResponse) -> Result<(), String> {
- let body = response.body.to_string();
- write!(
- stream,
- "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- response.status_code,
- status_text(response.status_code),
- body.len(),
- body
- )
- .map_err(|error| format!("write mock rpc response: {error}"))?;
- stream
- .flush()
- .map_err(|error| format!("flush mock rpc response: {error}"))
-}
-
-fn status_text(status_code: u16) -> &'static str {
- match status_code {
- 200 => "OK",
- 401 => "Unauthorized",
- 500 => "Internal Server Error",
- _ => "OK",
- }
-}
-
-fn sample_bridge_status() -> Value {
- json!({
- "enabled": true,
- "ready": true,
- "auth_mode": "bearer_token",
- "signer_mode": "selectable_per_request",
- "default_signer_mode": "embedded_service_identity",
- "supported_signer_modes": ["embedded_service_identity", "nip46_session"],
- "available_nip46_signer_sessions": 2,
- "relay_count": 3,
- "job_status_retention": 32,
- "retained_jobs": 1,
- "accepted_jobs": 4,
- "published_jobs": 3,
- "failed_jobs": 1,
- "recovered_failed_jobs": 0,
- "methods": ["bridge.status", "bridge.job.list", "bridge.job.status", "nip46.session.list"]
- })
-}
-
-fn sample_signer_session() -> Value {
- json!({
- "session_id": "sess_1",
- "role": "outbound_remote_signer",
- "client_pubkey": "client_pubkey",
- "signer_pubkey": "myc_signer_pubkey",
- "user_pubkey": "user_pubkey",
- "relays": ["wss://relay.one"],
- "permissions": ["sign_event", "nip44_encrypt"],
- "auth_required": false,
- "authorized": true,
- "auth_url": null,
- "expires_in_secs": 60,
- "signer_authority": {
- "provider_runtime_id": "myc",
- "account_identity_id": "acct_user",
- "provider_signer_session_id": "myc_conn_1"
- }
- })
-}
-
-fn sample_job(job_id: &str, state: &str, terminal: bool, completed_at_unix: Option<u64>) -> Value {
- json!({
- "job_id": job_id,
- "command": "bridge.listing.publish",
- "status": state,
- "terminal": terminal,
- "recovered_after_restart": false,
- "requested_at_unix": 1_712_720_000,
- "completed_at_unix": completed_at_unix,
- "signer_mode": "nip46_session",
- "signer_session_id": "session-1",
- "event_id": "event-123",
- "event_addr": "30023:npub1seller:listing-123",
- "delivery_policy": "best_effort",
- "delivery_quorum": 2,
- "relay_count": 3,
- "acknowledged_relay_count": if terminal { 2 } else { 1 },
- "required_acknowledged_relay_count": 2,
- "attempt_count": if terminal { 2 } else { 1 },
- "relay_outcome_summary": if terminal { "published to 2 relays" } else { "awaiting quorum" },
- "attempt_summaries": if terminal {
- json!(["attempt 1: relay.one accepted", "attempt 2: relay.two accepted"])
- } else {
- json!(["attempt 1: relay.one accepted"])
- }
- })
-}
-
-#[test]
-fn rpc_status_reports_bridge_ready_via_daemon_rpc() {
- let _guard = job_rpc_test_guard();
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |request| {
- let method = request.method.clone();
- recorded.lock().expect("record requests").push(request);
- match method.as_str() {
- "bridge.status" => MockRpcResponse::success(sample_bridge_status()),
- _ => MockRpcResponse::rpc_error(-32601, "method not found"),
- }
- });
-
- let dir = tempdir().expect("tempdir");
- let output = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args(["--json", "rpc", "status"])
- .output()
- .expect("run rpc status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["url"], server.url());
- assert_eq!(json["bridge_ready"], true);
- assert_eq!(json["retained_jobs"], 1);
- assert_eq!(json["session_surface_enabled"], true);
-
- let recorded = requests.lock().expect("recorded requests");
- assert_eq!(recorded.len(), 1);
- assert_eq!(recorded[0].method, "bridge.status");
- assert_eq!(recorded[0].auth_header.as_deref(), Some("Bearer secret"));
-}
-
-#[test]
-fn rpc_sessions_ndjson_emits_public_session_entries() {
- let _guard = job_rpc_test_guard();
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |request| {
- let method = request.method.clone();
- recorded.lock().expect("record requests").push(request);
- match method.as_str() {
- "nip46.session.list" => MockRpcResponse::success(json!([
- {
- "session_id": "session-1",
- "role": "client",
- "client_pubkey": "client-1",
- "signer_pubkey": "signer-1",
- "user_pubkey": "user-1",
- "relays": ["wss://relay.one"],
- "permissions": ["sign_event"],
- "auth_required": false,
- "authorized": true,
- "expires_in_secs": 60
- },
- {
- "session_id": "session-2",
- "role": "admin",
- "client_pubkey": "client-2",
- "signer_pubkey": "signer-2",
- "user_pubkey": null,
- "relays": ["wss://relay.two", "wss://relay.three"],
- "permissions": ["describe"],
- "auth_required": true,
- "authorized": false,
- "expires_in_secs": null
- }
- ])),
- _ => MockRpcResponse::rpc_error(-32601, "method not found"),
- }
- });
-
- let dir = tempdir().expect("tempdir");
- let output = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .args(["--ndjson", "rpc", "sessions"])
- .output()
- .expect("run rpc sessions");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines[0].contains("\"session_id\":\"session-1\""));
- assert!(lines[1].contains("\"authorized\":false"));
-
- let recorded = requests.lock().expect("recorded requests");
- assert_eq!(recorded.len(), 1);
- assert_eq!(recorded[0].method, "nip46.session.list");
- assert_eq!(recorded[0].auth_header, None);
-}
-
-#[test]
-fn signer_session_connect_preserves_configured_myc_authority() {
- let _guard = job_rpc_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let (bunker_json, bunker_request) = run_signer_session_command(
- dir.path(),
- &[
- "--json",
- "signer",
- "session",
- "connect-bunker",
- "bunker://remote",
- ],
- |_| {
- MockRpcResponse::success(json!({
- "session_id": "sess_bunker",
- "mode": "Bunker",
- "remote_signer_pubkey": "myc_signer_pubkey",
- "client_pubkey": "client_pubkey",
- "relays": ["wss://relay.one"]
- }))
- },
- );
- assert_eq!(bunker_json["action"], "connect_bunker");
- assert_eq!(bunker_json["session_id"], "sess_bunker");
- assert_eq!(bunker_json["remote_signer_pubkey"], "myc_signer_pubkey");
-
- let nostr_dir = tempdir().expect("tempdir");
- let (nostr_json, nostr_request) = run_signer_session_command(
- nostr_dir.path(),
- &[
- "--json",
- "signer",
- "session",
- "connect-nostrconnect",
- "nostrconnect://remote",
- "--client-secret-key",
- "client-secret",
- ],
- |_| {
- MockRpcResponse::success(json!({
- "session_id": "sess_nostrconnect",
- "mode": "Nostrconnect",
- "remote_signer_pubkey": "myc_signer_pubkey",
- "client_pubkey": "client_pubkey",
- "relays": ["wss://relay.two"]
- }))
- },
- );
- assert_eq!(nostr_json["action"], "connect_nostrconnect");
- assert_eq!(nostr_json["session_id"], "sess_nostrconnect");
-
- for request in [&bunker_request, &nostr_request] {
- assert_eq!(request.method, "nip46.connect");
- assert_eq!(request.auth_header.as_deref(), Some("Bearer secret"));
- assert_eq!(
- request.body["params"]["signer_authority"]["provider_runtime_id"],
- "myc"
- );
- assert_eq!(
- request.body["params"]["signer_authority"]["account_identity_id"],
- "acct_user"
- );
- assert_eq!(
- request.body["params"]["signer_authority"]["provider_signer_session_id"],
- "myc_conn_1"
- );
- }
- assert_eq!(bunker_request.body["params"]["url"], "bunker://remote");
- assert_eq!(nostr_request.body["params"]["url"], "nostrconnect://remote");
- assert_eq!(
- nostr_request.body["params"]["client_secret_key"],
- "client-secret"
- );
-}
-
-#[test]
-fn signer_session_commands_cover_inspect_hydrate_authorize_require_auth_and_close() {
- let _guard = job_rpc_test_guard();
- let dir = tempdir().expect("tempdir");
- let mut recorded = Vec::<MockRpcRequest>::new();
-
- let (list_json, request) =
- run_signer_session_command(dir.path(), &["--json", "signer", "session", "list"], |_| {
- MockRpcResponse::success(json!([sample_signer_session()]))
- });
- recorded.push(request);
- assert_eq!(list_json["state"], "ready");
- assert_eq!(list_json["sessions"][0]["session_id"], "sess_1");
- assert_eq!(list_json["sessions"][0]["user_pubkey"], "user_pubkey");
-
- let (show_json, request) = run_signer_session_command(
- dir.path(),
- &["--json", "signer", "session", "show", "sess_1"],
- |_| MockRpcResponse::success(sample_signer_session()),
- );
- recorded.push(request);
- assert_eq!(show_json["action"], "show");
- assert_eq!(show_json["session_id"], "sess_1");
- assert_eq!(show_json["user_pubkey"], "user_pubkey");
-
- let (public_key_json, request) = run_signer_session_command(
- dir.path(),
- &["--json", "signer", "session", "public-key", "sess_1"],
- |_| MockRpcResponse::success(json!({ "pubkey": "user_pubkey" })),
- );
- recorded.push(request);
- assert_eq!(public_key_json["action"], "public_key");
- assert_eq!(public_key_json["pubkey"], "user_pubkey");
-
- let (authorize_json, request) = run_signer_session_command(
- dir.path(),
- &["--json", "signer", "session", "authorize", "sess_1"],
- |_| MockRpcResponse::success(json!({ "authorized": true, "replayed": false })),
- );
- recorded.push(request);
- assert_eq!(authorize_json["action"], "authorize");
- assert_eq!(authorize_json["authorized"], true);
- assert_eq!(authorize_json["replayed"], false);
-
- let (require_auth_json, request) = run_signer_session_command(
- dir.path(),
- &[
- "--json",
- "signer",
- "session",
- "require-auth",
- "sess_1",
- "--auth-url",
- "https://auth.example",
- ],
- |_| MockRpcResponse::success(json!({ "required": true })),
- );
- recorded.push(request);
- assert_eq!(require_auth_json["action"], "require_auth");
- assert_eq!(require_auth_json["required"], true);
- assert_eq!(require_auth_json["auth_url"], "https://auth.example");
-
- let (close_json, request) = run_signer_session_command(
- dir.path(),
- &["--json", "signer", "session", "close", "sess_1"],
- |_| MockRpcResponse::success(json!({ "closed": true })),
- );
- recorded.push(request);
- assert_eq!(close_json["action"], "close");
- assert_eq!(close_json["closed"], true);
-
- let methods = recorded
- .iter()
- .map(|request| request.method.as_str())
- .collect::<Vec<_>>();
- assert_eq!(
- methods,
- [
- "nip46.session.list",
- "nip46.session.status",
- "nip46.get_public_key",
- "nip46.session.authorize",
- "nip46.session.require_auth",
- "nip46.session.close",
- ]
- );
- assert!(
- recorded
- .iter()
- .all(|request| request.auth_header.as_deref() == Some("Bearer secret"))
- );
- for request in recorded.iter().skip(1) {
- assert_eq!(request.body["params"]["session_id"], "sess_1");
- }
- assert_eq!(
- recorded[4].body["params"]["auth_url"],
- "https://auth.example"
- );
-}
-
-#[test]
-fn job_commands_require_bridge_bearer_token() {
- let _guard = job_rpc_test_guard();
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |request| {
- recorded.lock().expect("record requests").push(request);
- MockRpcResponse::rpc_error(-32601, "method not found")
- });
-
- let dir = tempdir().expect("tempdir");
- let output = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .args(["--json", "job", "ls"])
- .output()
- .expect("run job ls");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert!(
- json["reason"]
- .as_str()
- .expect("reason")
- .contains("bridge bearer token is not configured")
- );
- assert!(requests.lock().expect("recorded requests").is_empty());
-}
-
-#[test]
-fn job_ls_and_get_report_retained_bridge_jobs() {
- let _guard = job_rpc_test_guard();
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |request| {
- let method = request.method.clone();
- recorded.lock().expect("record requests").push(request);
- match method.as_str() {
- "bridge.job.list" => {
- MockRpcResponse::success(json!([sample_job("job-123", "publishing", false, None)]))
- }
- "bridge.job.status" => MockRpcResponse::success(sample_job(
- "job-123",
- "published",
- true,
- Some(1_712_720_030),
- )),
- _ => MockRpcResponse::rpc_error(-32601, "method not found"),
- }
- });
-
- let dir = tempdir().expect("tempdir");
- let list = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args(["--json", "job", "ls"])
- .output()
- .expect("run job ls");
- assert!(list.status.success());
- let list_json: Value = serde_json::from_slice(list.stdout.as_slice()).expect("list json");
- assert_eq!(list_json["state"], "ready");
- assert_eq!(list_json["count"], 1);
- assert_eq!(list_json["jobs"][0]["id"], "job-123");
- assert_eq!(list_json["jobs"][0]["command"], "listing.publish");
- assert_eq!(list_json["jobs"][0]["signer"], "nip46_session");
- assert_eq!(list_json["jobs"][0]["signer_session_id"], "session-1");
-
- let get = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args(["--json", "job", "get", "job-123"])
- .output()
- .expect("run job get");
- assert!(get.status.success());
- let get_json: Value = serde_json::from_slice(get.stdout.as_slice()).expect("get json");
- assert_eq!(get_json["state"], "ready");
- assert_eq!(get_json["job"]["id"], "job-123");
- assert_eq!(get_json["job"]["signer"], "nip46_session");
- assert_eq!(get_json["job"]["signer_session_id"], "session-1");
- assert_eq!(
- get_json["job"]["relay_outcome_summary"],
- "published to 2 relays"
- );
-
- let recorded = requests.lock().expect("recorded requests");
- assert_eq!(recorded.len(), 2);
- assert!(
- recorded
- .iter()
- .all(|request| request.auth_header.as_deref() == Some("Bearer secret"))
- );
-}
-
-#[test]
-fn job_watch_ndjson_emits_one_frame_per_poll_until_terminal() {
- let _guard = job_rpc_test_guard();
- let sequence = Arc::new(Mutex::new(0_usize));
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let observed = Arc::clone(&requests);
- let counter = Arc::clone(&sequence);
- let server = MockRpcServer::start(move |request| {
- let method = request.method.clone();
- observed.lock().expect("record requests").push(request);
- match method.as_str() {
- "bridge.job.status" => {
- let mut count = counter.lock().expect("watch count");
- *count += 1;
- if *count == 1 {
- MockRpcResponse::success(sample_job("job-123", "publishing", false, None))
- } else {
- MockRpcResponse::success(sample_job(
- "job-123",
- "published",
- true,
- Some(1_712_720_030),
- ))
- }
- }
- _ => MockRpcResponse::rpc_error(-32601, "method not found"),
- }
- });
-
- let dir = tempdir().expect("tempdir");
- let output = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args([
- "--ndjson",
- "job",
- "watch",
- "job-123",
- "--frames",
- "3",
- "--interval-ms",
- "5",
- ])
- .output()
- .expect("run job watch");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines[0].contains("\"sequence\":1"));
- assert!(lines[0].contains("\"state\":\"publishing\""));
- assert!(lines[0].contains("\"signer\":\"nip46_session\""));
- assert!(lines[0].contains("\"signer_session_id\":\"session-1\""));
- assert!(lines[1].contains("\"sequence\":2"));
- assert!(lines[1].contains("\"terminal\":true"));
- assert!(lines[1].contains("\"signer_session_id\":\"session-1\""));
-}
-
-#[test]
-fn job_watch_human_appends_snapshots_without_screen_clear() {
- let _guard = job_rpc_test_guard();
- let sequence = Arc::new(Mutex::new(0_usize));
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let observed = Arc::clone(&requests);
- let counter = Arc::clone(&sequence);
- let server = MockRpcServer::start(move |request| {
- let method = request.method.clone();
- observed.lock().expect("record requests").push(request);
- match method.as_str() {
- "bridge.job.status" => {
- let mut count = counter.lock().expect("watch count");
- *count += 1;
- if *count == 1 {
- MockRpcResponse::success(sample_job("job-123", "publishing", false, None))
- } else {
- MockRpcResponse::success(sample_job(
- "job-123",
- "published",
- true,
- Some(1_712_720_030),
- ))
- }
- }
- _ => MockRpcResponse::rpc_error(-32601, "method not found"),
- }
- });
-
- let dir = tempdir().expect("tempdir");
- let output = job_rpc_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args([
- "job",
- "watch",
- "job-123",
- "--frames",
- "3",
- "--interval-ms",
- "5",
- ])
- .output()
- .expect("run human job watch");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Watching job job-123"));
- assert!(stdout.contains("Publishing"));
- assert!(stdout.contains("Published"));
- assert!(stdout.contains("Summary"));
- assert!(stdout.contains("Signer"));
- assert!(!stdout.contains("activity ยท"));
- assert!(!stdout.contains("\u{1b}"));
-}
diff --git a/tests/listing.rs b/tests/listing.rs
@@ -1,1860 +0,0 @@
-use std::fs;
-use std::io::{Read, Write};
-use std::net::{TcpListener, TcpStream};
-use std::os::unix::fs::PermissionsExt;
-use std::path::Path;
-use std::process::Command;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
-use std::thread::{self, JoinHandle};
-use std::time::Duration;
-
-use assert_cmd::prelude::*;
-use radroots_identity::RadrootsIdentity;
-use radroots_sql_core::{SqlExecutor, SqliteExecutor};
-use serde_json::{Value, json};
-use tempfile::tempdir;
-
-const ADDRESS_BACKED_LISTING_ADDR: &str =
- "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn write_user_config(workdir: &Path, contents: &str) {
- let config_dir = workdir.join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(config_dir.join("config.toml"), contents).expect("write user config");
-}
-
-fn config_with_write_plane(extra: &str, url: &str) -> String {
- let mut rendered = String::new();
- if !extra.trim().is_empty() {
- rendered.push_str(extra.trim());
- rendered.push_str("\n\n");
- }
- rendered.push_str(
- format!(
- r#"[[capability_binding]]
-capability = "write_plane.trade_jsonrpc"
-provider = "radrootsd"
-target_kind = "explicit_endpoint"
-target = "{url}"
-"#
- )
- .as_str(),
- );
- rendered
-}
-
-fn write_fake_myc(dir: &Path, script: &str) -> std::path::PathBuf {
- let path = dir.join("fake-myc");
- fs::write(&path, script).expect("write fake myc");
- let mut permissions = fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("chmod fake myc");
- path
-}
-
-fn successful_status_script(payload_json: String) -> String {
- format!(
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"signer\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\ncat <<'JSON'\n{payload_json}\nJSON\n"
- )
-}
-
-fn sample_myc_status_payload(
- account_id: &str,
- public_identity: &Value,
- connection_id: &str,
-) -> Value {
- let signer_identity =
- serde_json::to_value(RadrootsIdentity::generate().to_public()).expect("signer identity");
- let signer_account_id = signer_identity["id"]
- .as_str()
- .expect("signer id")
- .to_owned();
- assert_ne!(signer_account_id, account_id);
- json!({
- "status_contract_version": 1,
- "status": "healthy",
- "ready": true,
- "reasons": [],
- "signer_backend": {
- "local_signer": {
- "account_id": signer_account_id,
- "public_identity": signer_identity.clone(),
- "availability": "SecretBacked"
- },
- "remote_session_count": 1,
- "remote_sessions": [
- {
- "connection_id": connection_id,
- "signer_identity": signer_identity,
- "user_identity": public_identity,
- "relays": ["wss://relay.one"],
- "permissions": "sign_event"
- }
- ]
- }
- })
-}
-
-fn listing_test_guard() -> MutexGuard<'static, ()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(()))
- .lock()
- .expect("listing test lock")
-}
-
-#[test]
-fn listing_new_uses_selected_farm_config_and_product_inputs() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey");
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
-
- let setup_output = cli_command_in(dir.path())
- .args([
- "--json",
- "farm",
- "setup",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--city",
- "San Francisco",
- "--region",
- "CA",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ])
- .output()
- .expect("run farm setup");
- assert!(setup_output.status.success());
- let setup_json: Value =
- serde_json::from_slice(setup_output.stdout.as_slice()).expect("setup json");
- let farm_d_tag = setup_json["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d_tag")
- .to_owned();
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "listing",
- "new",
- "--key",
- "sf-tomatoes",
- "--title",
- "San Francisco Early Girl Tomatoes",
- "--category",
- "produce.vegetables.tomatoes",
- "--summary",
- "Fresh local tomatoes packed for pickup from the seller's standard market location.",
- "--quantity-amount",
- "1000",
- "--quantity-unit",
- "g",
- "--price-amount",
- "0.01",
- "--available",
- "25",
- "--label",
- "1 kg tomato lot",
- ])
- .output()
- .expect("run listing new");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "draft created");
- assert_eq!(json["selected_account_id"], account_id);
- assert_eq!(json["seller_pubkey"], seller_pubkey);
- assert_eq!(json["farm_d_tag"], farm_d_tag);
- assert_eq!(json["delivery_method"], "pickup");
- assert_eq!(json["location_primary"], "San Francisco, CA");
- let file = json["file"].as_str().expect("draft file");
- let contents = fs::read_to_string(file).expect("draft contents");
- assert!(contents.contains("kind = \"listing_draft_v1\""));
- assert!(contents.contains(&format!("seller_pubkey = \"{seller_pubkey}\"")));
- assert!(contents.contains(&format!("farm_d_tag = \"{farm_d_tag}\"")));
- assert!(contents.contains("key = \"sf-tomatoes\""));
- assert!(contents.contains("title = \"San Francisco Early Girl Tomatoes\""));
- assert!(contents.contains("category = \"produce.vegetables.tomatoes\""));
- assert!(contents.contains("method = \"pickup\""));
- assert!(contents.contains("primary = \"San Francisco, CA\""));
- assert!(contents.contains("price_currency = \"USD\""));
- assert!(contents.contains("price_per_amount = \"1\""));
- assert!(contents.contains("price_per_unit = \"g\""));
-}
-
-#[test]
-fn listing_validate_resolves_selected_farm_config_defaults() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- let setup_output = cli_command_in(dir.path())
- .args([
- "--json",
- "farm",
- "setup",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--delivery-method",
- "pickup",
- ])
- .output()
- .expect("run farm setup");
- assert!(setup_output.status.success());
- let setup_json: Value =
- serde_json::from_slice(setup_output.stdout.as_slice()).expect("setup json");
- let farm_d_tag = setup_json["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d_tag")
- .to_owned();
-
- let draft_path = dir.path().join("eggs.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "listing",
- "validate",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing validate");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "valid");
- assert_eq!(json["valid"], true);
- assert_eq!(json["seller_pubkey"], seller_pubkey);
- assert_eq!(json["farm_d_tag"], farm_d_tag);
-}
-
-#[test]
-fn listing_validate_reports_invalid_drafts_with_field_lines() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let draft_path = dir.path().join("invalid.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "AAAAAAAAAAAAAAAAAAAAAw",
- &"b".repeat(64),
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "oops",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write invalid draft");
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "listing",
- "validate",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing validate");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "invalid");
- assert_eq!(json["valid"], false);
- assert_eq!(json["issues"][0]["field"], "primary_bin.price_amount");
- assert!(json["issues"][0]["line"].as_u64().is_some());
-}
-
-#[test]
-fn listing_get_reads_real_local_rows_and_reports_missing() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000301",
- "pasture-eggs",
- Some(ADDRESS_BACKED_LISTING_ADDR),
- "protein",
- "Pasture Eggs",
- "Fresh pasture-raised eggs collected daily.",
- 36,
- 18,
- Some("Marshall"),
- );
-
- let json_output = cli_command_in(dir.path())
- .args(["--json", "listing", "get", "pasture-eggs"])
- .output()
- .expect("run listing get");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["product_key"], "pasture-eggs");
- assert_eq!(json["listing_addr"], ADDRESS_BACKED_LISTING_ADDR);
- assert_eq!(json["title"], "Pasture Eggs");
- assert_eq!(json["location_primary"], "Marshall");
- assert_eq!(json["provenance"]["origin"], "local_replica.trade_product");
-
- let human_output = cli_command_in(dir.path())
- .args(["listing", "get", "pasture-eggs"])
- .output()
- .expect("run human listing get");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Pasture Eggs"));
- assert!(stdout.contains("Listing"));
- assert!(stdout.contains("Key"));
- assert!(stdout.contains("Place"));
- assert!(stdout.contains("About"));
- assert!(!stdout.contains("listing ยท"));
- assert!(!stdout.contains("provenance:"));
-
- let missing_output = cli_command_in(dir.path())
- .args(["--json", "listing", "get", "missing-listing"])
- .output()
- .expect("run missing listing get");
- assert!(missing_output.status.success());
- let missing_json: Value =
- serde_json::from_slice(missing_output.stdout.as_slice()).expect("json");
- assert_eq!(missing_json["state"], "missing");
-}
-
-#[test]
-fn listing_publish_and_update_use_durable_bridge_publish() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
- seed_farm(dir.path(), seller_pubkey.as_str(), farm_d_tag, "La Huerta");
-
- let draft_path = dir.path().join("eggs.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, auth_header| {
- recorded.lock().expect("recorded").push(body.clone());
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => {
- assert_eq!(auth_header, None);
- MockRpcResponse::success(json!([sample_session_with_authority(
- "sess_publish_01",
- seller_pubkey.as_str(),
- &["sign_event"],
- true,
- Some(account_id.as_str()),
- Some("conn_listing_binding_01")
- )]))
- }
- "bridge.listing.publish" => {
- assert_eq!(auth_header.as_deref(), Some("Bearer bridge-secret"));
- MockRpcResponse::success(json!({
- "deduplicated": false,
- "job": sample_listing_job(
- "job_listing_01",
- "published",
- "event_listing_01",
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "sess_publish_01"
- )
- }))
- }
- other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "listing",
- "publish",
- "--idempotency-key",
- "publish-key",
- "--signer-session-id",
- "sess_publish_01",
- "--print-job",
- "--print-event",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
- assert!(publish_output.status.success());
- let publish_json: Value =
- serde_json::from_slice(publish_output.stdout.as_slice()).expect("publish json");
- assert_eq!(publish_json["operation"], "publish");
- assert_eq!(publish_json["job_id"], "job_listing_01");
- assert_eq!(publish_json["job_status"], "published");
- assert_eq!(publish_json["event_id"], "event_listing_01");
- assert_eq!(publish_json["event"]["kind"], 30402);
- assert_eq!(publish_json["signer_mode"], "nip46_session");
- assert_eq!(publish_json["signer_session_id"], "sess_publish_01");
- assert_eq!(publish_json["job"]["rpc_method"], "bridge.listing.publish");
- assert_eq!(publish_json["job"]["signer_mode"], "nip46_session");
- assert_eq!(publish_json["job"]["signer_session_id"], "sess_publish_01");
- assert_eq!(
- publish_json["requested_signer_session_id"],
- "sess_publish_01"
- );
- assert_eq!(
- publish_json["job"]["requested_signer_session_id"],
- "sess_publish_01"
- );
-
- let update_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "listing",
- "update",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing update");
- assert!(update_output.status.success());
- let update_json: Value =
- serde_json::from_slice(update_output.stdout.as_slice()).expect("update json");
- assert_eq!(update_json["operation"], "update");
-
- let recorded = requests.lock().expect("requests");
- assert_eq!(recorded.len(), 4);
- assert_eq!(recorded[0]["method"], "nip46.session.list");
- assert_eq!(recorded[1]["params"]["kind"], 30402);
- assert_eq!(recorded[1]["params"]["idempotency_key"], "publish-key");
- assert_eq!(
- recorded[1]["params"]["signer_session_id"],
- "sess_publish_01"
- );
- assert_eq!(recorded[2]["method"], "nip46.session.list");
- assert_eq!(recorded[3]["params"]["kind"], 30402);
- assert_eq!(
- recorded[3]["params"]["signer_session_id"],
- "sess_publish_01"
- );
-}
-
-#[test]
-fn listing_archive_and_dry_run_are_truthful() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let draft_path = dir.path().join("archive.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let requests = Arc::new(Mutex::new(Vec::<String>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded.lock().expect("recorded").push(body.to_string());
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_archive_01",
- seller_pubkey.as_str(),
- &["sign_event"],
- true
- )])),
- "bridge.listing.publish" => MockRpcResponse::success(json!({
- "deduplicated": false,
- "job": sample_listing_job(
- "job_listing_archive",
- "published",
- "event_listing_archive",
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "sess_archive_01"
- )
- })),
- other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let archive_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "listing",
- "archive",
- "--signer-session-id",
- "sess_archive_01",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing archive");
- assert!(archive_output.status.success());
- let archive_json: Value =
- serde_json::from_slice(archive_output.stdout.as_slice()).expect("archive json");
- assert_eq!(archive_json["operation"], "archive");
- assert_eq!(archive_json["job_status"], "published");
- assert_eq!(archive_json["signer_mode"], "nip46_session");
- assert_eq!(archive_json["signer_session_id"], "sess_archive_01");
- assert_eq!(
- archive_json["requested_signer_session_id"],
- "sess_archive_01"
- );
-
- let dry_run_output = cli_command_in(dir.path())
- .args([
- "--json",
- "--dry-run",
- "listing",
- "publish",
- "--signer-session-id",
- "sess_dry_run_01",
- "--print-event",
- "--print-job",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish dry run");
- assert!(dry_run_output.status.success());
- let dry_run_json: Value =
- serde_json::from_slice(dry_run_output.stdout.as_slice()).expect("dry run json");
- assert_eq!(dry_run_json["state"], "dry_run");
- assert_eq!(dry_run_json["dry_run"], true);
- assert_eq!(dry_run_json["job"]["state"], "not_submitted");
- assert!(dry_run_json["signer_mode"].is_null());
- assert!(dry_run_json["signer_session_id"].is_null());
- assert_eq!(dry_run_json["job"]["signer_mode"], "local");
- assert!(dry_run_json["job"]["signer_session_id"].is_null());
- assert_eq!(dry_run_json["event"]["kind"], 30402);
- assert!(dry_run_json["event"]["event_id"].is_null());
- assert_eq!(
- dry_run_json["requested_signer_session_id"],
- "sess_dry_run_01"
- );
- assert_eq!(
- dry_run_json["job"]["requested_signer_session_id"],
- "sess_dry_run_01"
- );
-
- let recorded = requests.lock().expect("requests");
- assert_eq!(recorded.len(), 2);
- assert!(recorded[1].contains("archived"));
-}
-
-#[test]
-fn listing_publish_uses_myc_binding_before_resolving_daemon_signer_session() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
- let public_identity = account_json["public_identity"].clone();
- let seller_pubkey = public_identity["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let draft_path = dir.path().join("myc-listing.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "AAAAAAAAAAAAAAAAAAAAAw",
- seller_pubkey.as_str(),
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let myc = write_fake_myc(
- dir.path(),
- successful_status_script(
- sample_myc_status_payload(
- account_id.as_str(),
- &public_identity,
- "conn_listing_binding_01",
- )
- .to_string(),
- )
- .as_str(),
- );
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let session_account_id = account_id.clone();
- let provider_pubkey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
- let server = MockRpcServer::start(move |body, auth_header| {
- recorded.lock().expect("recorded").push(body.clone());
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => {
- assert_eq!(auth_header, None);
- let mut session = sample_session_with_authority(
- "sess_publish_01",
- provider_pubkey,
- &["sign_event"],
- true,
- Some(session_account_id.as_str()),
- Some("conn_listing_binding_01"),
- );
- session["user_pubkey"] = Value::Null;
- MockRpcResponse::success(json!([session]))
- }
- "nip46.get_public_key" => {
- assert_eq!(auth_header.as_deref(), Some("Bearer bridge-secret"));
- assert_eq!(body["params"]["session_id"], "sess_publish_01");
- MockRpcResponse::success(json!({
- "pubkey": seller_pubkey.as_str()
- }))
- }
- "bridge.listing.publish" => {
- assert_eq!(auth_header.as_deref(), Some("Bearer bridge-secret"));
- MockRpcResponse::success(json!({
- "deduplicated": false,
- "job": sample_listing_job(
- "job_listing_02",
- "published",
- "event_listing_02",
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "sess_publish_01"
- )
- }))
- }
- other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane(
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{account_id}"
-"#
- )
- .as_str(),
- server.url().as_str(),
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- myc.to_str().expect("myc path"),
- "listing",
- "publish",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
-
- assert!(
- output.status.success(),
- "stdout:\n{}\n\nstderr:\n{}",
- String::from_utf8_lossy(output.stdout.as_slice()),
- String::from_utf8_lossy(output.stderr.as_slice())
- );
- let publish_json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(publish_json["state"], "published");
- assert_eq!(publish_json["signer_mode"], "nip46_session");
- assert_eq!(publish_json["signer_session_id"], "sess_publish_01");
- assert_eq!(publish_json["requested_signer_session_id"], Value::Null);
-
- let recorded = requests.lock().expect("requests");
- assert_eq!(recorded.len(), 3);
- assert_eq!(recorded[0]["method"], "nip46.session.list");
- assert_eq!(recorded[1]["method"], "nip46.get_public_key");
- assert_eq!(recorded[2]["method"], "bridge.listing.publish");
- assert_eq!(
- recorded[2]["params"]["signer_session_id"],
- "sess_publish_01"
- );
- assert_eq!(
- recorded[2]["params"]["signer_authority"]["provider_runtime_id"],
- "myc"
- );
- assert_eq!(
- recorded[2]["params"]["signer_authority"]["account_identity_id"],
- account_id
- );
- assert_eq!(
- recorded[2]["params"]["signer_authority"]["provider_signer_session_id"],
- "conn_listing_binding_01"
- );
-}
-
-#[test]
-fn listing_publish_rejects_myc_binding_that_resolves_the_wrong_actor() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let mismatch_account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run mismatch account new");
- assert!(mismatch_account_output.status.success());
- let mismatch_account_json: Value =
- serde_json::from_slice(mismatch_account_output.stdout.as_slice()).expect("mismatch json");
- let mismatch_account_id = mismatch_account_json["account"]["id"]
- .as_str()
- .expect("mismatch account id");
- let mismatch_public_identity = mismatch_account_json["public_identity"].clone();
-
- let draft_path = dir.path().join("wrong-myc-listing.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "AAAAAAAAAAAAAAAAAAAAAw",
- seller_pubkey.as_str(),
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let myc = write_fake_myc(
- dir.path(),
- successful_status_script(
- sample_myc_status_payload(
- mismatch_account_id,
- &mismatch_public_identity,
- "conn_listing_binding_02",
- )
- .to_string(),
- )
- .as_str(),
- );
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded.lock().expect("recorded").push(body.clone());
- MockRpcResponse::rpc_error(-32601, "daemon write path should not be reached")
- });
- write_user_config(
- dir.path(),
- config_with_write_plane(
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{mismatch_account_id}"
-"#
- )
- .as_str(),
- server.url().as_str(),
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- myc.to_str().expect("myc path"),
- "listing",
- "publish",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
-
- assert_eq!(output.status.code(), Some(3));
- let publish_json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(publish_json["state"], "unconfigured");
- assert_eq!(publish_json["signer_mode"], "myc");
- assert!(publish_json["reason"].as_str().is_some_and(|value| {
- value.contains("configured myc signer binding resolves user pubkey")
- }));
- assert!(requests.lock().expect("requests").is_empty());
-}
-
-#[test]
-fn listing_publish_rejects_daemon_session_with_mismatched_myc_authority() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
- let public_identity = account_json["public_identity"].clone();
- let seller_pubkey = public_identity["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let draft_path = dir.path().join("mismatched-authority.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let myc = write_fake_myc(
- dir.path(),
- successful_status_script(
- sample_myc_status_payload(
- account_id.as_str(),
- &public_identity,
- "conn_listing_binding_03",
- )
- .to_string(),
- )
- .as_str(),
- );
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded.lock().expect("recorded").push(body.clone());
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => {
- MockRpcResponse::success(json!([sample_session_with_authority(
- "sess_mismatch_01",
- seller_pubkey.as_str(),
- &["sign_event:30402"],
- true,
- Some("acct_wrong"),
- Some("conn_listing_binding_03"),
- )]))
- }
- _ => MockRpcResponse::rpc_error(-32601, "unexpected rpc method"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane(
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{account_id}"
-"#
- )
- .as_str(),
- server.url().as_str(),
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- myc.to_str().expect("myc path"),
- "listing",
- "publish",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
-
- assert_eq!(output.status.code(), Some(3));
- let publish_json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(publish_json["state"], "unconfigured");
- assert!(publish_json["reason"].as_str().is_some());
-
- let recorded = requests.lock().expect("requests");
- assert_eq!(recorded.len(), 1);
- assert_eq!(recorded[0]["method"], "nip46.session.list");
-}
-
-#[test]
-fn listing_publish_without_matching_signer_session_exits_unconfigured() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let draft_path = dir.path().join("no-session.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded.lock().expect("recorded").push(body.clone());
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_other_01",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- &["sign_event"],
- true
- )])),
- other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "listing",
- "publish",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
- assert_eq!(publish_output.status.code(), Some(3));
- let publish_json: Value =
- serde_json::from_slice(publish_output.stdout.as_slice()).expect("publish json");
- assert_eq!(publish_json["state"], "unconfigured");
- assert!(
- publish_json["reason"]
- .as_str()
- .expect("reason")
- .contains("no authorized signer session matched seller pubkey")
- );
-
- let recorded = requests.lock().expect("requests");
- assert_eq!(recorded.len(), 1);
- assert_eq!(recorded[0]["method"], "nip46.session.list");
-}
-
-#[test]
-fn listing_publish_rejects_requested_session_that_mismatches_seller_pubkey() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let draft_path = dir.path().join("mismatch-session.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded.lock().expect("recorded").push(body.clone());
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_wrong_01",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- &["sign_event"],
- true
- )])),
- other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "listing",
- "publish",
- "--signer-session-id",
- "sess_wrong_01",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
- assert_eq!(publish_output.status.code(), Some(3));
- let publish_json: Value =
- serde_json::from_slice(publish_output.stdout.as_slice()).expect("publish json");
- assert_eq!(publish_json["state"], "unconfigured");
- assert!(
- publish_json["reason"]
- .as_str()
- .expect("reason")
- .contains("does not match seller pubkey")
- );
-
- let recorded = requests.lock().expect("requests");
- assert_eq!(recorded.len(), 1);
- assert_eq!(recorded[0]["method"], "nip46.session.list");
-}
-
-#[test]
-fn listing_publish_requires_authoritative_write_plane_binding() {
- let _guard = listing_test_guard();
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let account_output = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let seller_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("seller pubkey")
- .to_owned();
- seed_farm(
- dir.path(),
- seller_pubkey.as_str(),
- "AAAAAAAAAAAAAAAAAAAAAw",
- "La Huerta",
- );
-
- let draft_path = dir.path().join("missing-write-binding.toml");
- fs::write(
- &draft_path,
- valid_listing_draft(
- "AAAAAAAAAAAAAAAAAAAAAg",
- "",
- "",
- "eggs",
- "Pasture eggs",
- "Protein",
- "Fresh pasture-raised eggs collected daily.",
- "12",
- "each",
- "4.50",
- "USD",
- "1",
- "each",
- "18",
- "pickup",
- "La Huerta del Sur",
- ),
- )
- .expect("write listing draft");
-
- let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded.lock().expect("recorded").push(body);
- MockRpcResponse::rpc_error(-32601, "daemon write path should not be reached")
- });
-
- let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
- .args([
- "--json",
- "listing",
- "publish",
- draft_path.to_str().expect("draft path"),
- ])
- .output()
- .expect("run listing publish");
- assert_eq!(publish_output.status.code(), Some(3));
- let publish_json: Value =
- serde_json::from_slice(publish_output.stdout.as_slice()).expect("publish json");
- assert_eq!(publish_json["state"], "unconfigured");
- assert!(
- publish_json["reason"].as_str().expect("reason").contains(
- "explicit write-plane capability binding or managed radrootsd instance `local`"
- )
- );
- assert!(requests.lock().expect("requests").is_empty());
-}
-
-fn seed_farm(workdir: &Path, pubkey: &str, d_tag: &str, name: &str) {
- let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
- let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
- let now = "2026-04-07T00:00:00.000Z";
- executor
- .exec(
- "INSERT INTO farm (id, created_at, updated_at, d_tag, pubkey, name, about, website, picture, banner, location_primary, location_city, location_region, location_country) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- "11111111-1111-1111-1111-111111111111",
- now,
- now,
- d_tag,
- pubkey,
- name,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert farm");
-}
-
-#[derive(Debug, Clone)]
-struct MockRpcRequest {
- body: Value,
- auth_header: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-struct MockRpcResponse {
- body: Value,
-}
-
-impl MockRpcResponse {
- fn success(result: Value) -> Self {
- Self {
- body: json!({
- "jsonrpc": "2.0",
- "id": 1,
- "result": result,
- }),
- }
- }
-
- fn rpc_error(code: i64, message: &str) -> Self {
- Self {
- body: json!({
- "jsonrpc": "2.0",
- "id": 1,
- "error": {
- "code": code,
- "message": message,
- }
- }),
- }
- }
-}
-
-struct MockRpcServer {
- address: String,
- shutdown: Arc<AtomicBool>,
- handle: Option<JoinHandle<()>>,
-}
-
-impl MockRpcServer {
- fn start<F>(handler: F) -> Self
- where
- F: Fn(Value, Option<String>) -> MockRpcResponse + Send + Sync + 'static,
- {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock rpc listener");
- listener
- .set_nonblocking(true)
- .expect("set listener nonblocking");
- let address = listener.local_addr().expect("local addr").to_string();
- let shutdown = Arc::new(AtomicBool::new(false));
- let shutdown_flag = Arc::clone(&shutdown);
- let handler: Arc<dyn Fn(Value, Option<String>) -> MockRpcResponse + Send + Sync> =
- Arc::new(handler);
- let handle = thread::spawn(move || {
- while !shutdown_flag.load(Ordering::SeqCst) {
- match listener.accept() {
- Ok((mut stream, _)) => {
- let _ = stream.set_nonblocking(false);
- if let Ok(request) = read_request(&mut stream) {
- let response =
- handler(request.body.clone(), request.auth_header.clone());
- let _ = write_response(&mut stream, &response);
- }
- }
- Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
- thread::sleep(Duration::from_millis(10));
- }
- Err(_) => {
- thread::sleep(Duration::from_millis(10));
- }
- }
- }
- });
- Self {
- address,
- shutdown,
- handle: Some(handle),
- }
- }
-
- fn url(&self) -> String {
- format!("http://{}", self.address)
- }
-}
-
-impl Drop for MockRpcServer {
- fn drop(&mut self) {
- self.shutdown.store(true, Ordering::SeqCst);
- let _ = TcpStream::connect(&self.address);
- if let Some(handle) = self.handle.take() {
- handle.join().expect("join mock rpc server");
- }
- }
-}
-
-fn read_request(stream: &mut TcpStream) -> Result<MockRpcRequest, String> {
- stream
- .set_read_timeout(Some(Duration::from_secs(2)))
- .map_err(|error| format!("set mock rpc read timeout: {error}"))?;
- let mut buffer = Vec::new();
- let mut chunk = [0_u8; 4096];
- let mut header_end = None;
- let mut content_length = 0usize;
-
- loop {
- let read = stream
- .read(&mut chunk)
- .map_err(|error| format!("read mock rpc request: {error}"))?;
- if read == 0 {
- break;
- }
- buffer.extend_from_slice(&chunk[..read]);
- if header_end.is_none() {
- header_end = find_subslice(&buffer, b"\r\n\r\n").map(|index| index + 4);
- if let Some(end) = header_end {
- content_length = parse_content_length(&buffer[..end])?;
- if buffer.len() >= end + content_length {
- break;
- }
- }
- } else if let Some(end) = header_end {
- if buffer.len() >= end + content_length {
- break;
- }
- }
- }
-
- let end = header_end.ok_or_else(|| "mock rpc request missing headers".to_owned())?;
- let headers = std::str::from_utf8(&buffer[..end])
- .map_err(|error| format!("mock rpc headers not utf-8: {error}"))?;
- let auth_header = parse_header(headers, "authorization");
- let body = std::str::from_utf8(&buffer[end..end + content_length])
- .map_err(|error| format!("mock rpc body not utf-8: {error}"))?;
- let json: Value =
- serde_json::from_str(body).map_err(|error| format!("parse mock rpc body: {error}"))?;
-
- Ok(MockRpcRequest {
- body: json,
- auth_header,
- })
-}
-
-fn parse_content_length(headers: &[u8]) -> Result<usize, String> {
- let text = std::str::from_utf8(headers)
- .map_err(|error| format!("header utf-8 parse failed: {error}"))?;
- for line in text.lines() {
- if let Some((name, value)) = line.split_once(':') {
- if name.trim().eq_ignore_ascii_case("content-length") {
- return value
- .trim()
- .parse::<usize>()
- .map_err(|error| format!("content-length parse failed: {error}"));
- }
- }
- }
- Ok(0)
-}
-
-fn parse_header(headers: &str, wanted: &str) -> Option<String> {
- headers.lines().find_map(|line| {
- let (name, value) = line.split_once(':')?;
- if name.trim().eq_ignore_ascii_case(wanted) {
- Some(value.trim().to_owned())
- } else {
- None
- }
- })
-}
-
-fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
- haystack
- .windows(needle.len())
- .position(|window| window == needle)
-}
-
-fn write_response(stream: &mut TcpStream, response: &MockRpcResponse) -> Result<(), String> {
- let body = response.body.to_string();
- write!(
- stream,
- "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- )
- .map_err(|error| format!("write mock rpc response: {error}"))?;
- stream
- .flush()
- .map_err(|error| format!("flush mock rpc response: {error}"))
-}
-
-fn sample_listing_job(
- job_id: &str,
- status: &str,
- event_id: &str,
- event_addr: &str,
- signer_session_id: &str,
-) -> Value {
- json!({
- "job_id": job_id,
- "command": "bridge.listing.publish",
- "idempotency_key": "publish-key",
- "status": status,
- "terminal": status != "accepted",
- "recovered_after_restart": false,
- "requested_at_unix": 1_712_720_000,
- "completed_at_unix": 1_712_720_010,
- "signer_mode": "nip46_session",
- "signer_session_id": signer_session_id,
- "event_kind": 30402,
- "event_id": event_id,
- "event_addr": event_addr,
- "delivery_policy": "best_effort",
- "delivery_quorum": 2,
- "relay_count": 2,
- "acknowledged_relay_count": 2,
- "required_acknowledged_relay_count": 2,
- "attempt_count": 1,
- "attempt_summaries": ["attempt 1: relay.one accepted"],
- "relay_results": [],
- "relay_outcome_summary": "published to 2 relays"
- })
-}
-
-fn sample_session(
- session_id: &str,
- signer_pubkey: &str,
- permissions: &[&str],
- authorized: bool,
-) -> Value {
- sample_session_with_authority(
- session_id,
- signer_pubkey,
- permissions,
- authorized,
- None,
- None,
- )
-}
-
-fn sample_session_with_authority(
- session_id: &str,
- signer_pubkey: &str,
- permissions: &[&str],
- authorized: bool,
- account_identity_id: Option<&str>,
- provider_signer_session_id: Option<&str>,
-) -> Value {
- json!({
- "session_id": session_id,
- "role": "remote_signer",
- "client_pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
- "signer_pubkey": signer_pubkey,
- "user_pubkey": signer_pubkey,
- "relays": ["wss://relay.one"],
- "permissions": permissions,
- "auth_required": false,
- "authorized": authorized,
- "expires_in_secs": Value::Null,
- "signer_authority": account_identity_id.map(|account_identity_id| json!({
- "provider_runtime_id": "myc",
- "account_identity_id": account_identity_id,
- "provider_signer_session_id": provider_signer_session_id
- }))
- })
-}
-
-fn seed_trade_product(
- workdir: &Path,
- product_id: &str,
- key: &str,
- listing_addr: Option<&str>,
- category: &str,
- title: &str,
- summary: &str,
- qty_amt: i64,
- qty_avail: i64,
- location_label: Option<&str>,
-) {
- let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
- let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
- let now = "2026-04-07T00:00:00.000Z";
- executor
- .exec(
- "INSERT INTO trade_product (id, created_at, updated_at, key, listing_addr, 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- product_id,
- now,
- now,
- key,
- listing_addr,
- category,
- title,
- summary,
- "fresh",
- "lot-a",
- "standard",
- 2026,
- qty_amt,
- "each",
- "dozen",
- qty_avail,
- 4.5,
- "USD",
- 1,
- "each",
- Value::Null
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert trade product");
-
- if let Some(location_label) = location_label {
- let location_id = format!("11111111-1111-1111-1111-{}", &product_id[24..]);
- executor
- .exec(
- "INSERT INTO gcs_location (id, created_at, updated_at, d_tag, lat, lng, geohash, point, polygon, accuracy, altitude, tag_0, label, area, elevation, soil, climate, gc_id, gc_name, gc_admin1_id, gc_admin1_name, gc_country_id, gc_country_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- location_id,
- now,
- now,
- format!("location-{product_id}"),
- 35.0,
- -82.0,
- "dnrj",
- "POINT(-82 35)",
- "POLYGON EMPTY",
- Value::Null,
- Value::Null,
- Value::Null,
- location_label,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- location_label,
- Value::Null,
- Value::Null,
- Value::Null,
- "USA"
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert gcs location");
- executor
- .exec(
- "INSERT INTO trade_product_location (tb_tp, tb_gl) VALUES (?, ?);",
- json!([product_id, location_id]).to_string().as_str(),
- )
- .expect("insert trade product location");
- }
-}
-
-fn valid_listing_draft(
- d_tag: &str,
- farm_d_tag: &str,
- seller_pubkey: &str,
- key: &str,
- title: &str,
- category: &str,
- summary: &str,
- quantity_amount: &str,
- quantity_unit: &str,
- price_amount: &str,
- price_currency: &str,
- price_per_amount: &str,
- price_per_unit: &str,
- available: &str,
- delivery_method: &str,
- location_primary: &str,
-) -> String {
- format!(
- "version = 1\nkind = \"listing_draft_v1\"\n\n[listing]\nd_tag = \"{d_tag}\"\nfarm_d_tag = \"{farm_d_tag}\"\nseller_pubkey = \"{seller_pubkey}\"\n\n[product]\nkey = \"{key}\"\ntitle = \"{title}\"\ncategory = \"{category}\"\nsummary = \"{summary}\"\n\n[primary_bin]\nbin_id = \"bin-1\"\nquantity_amount = \"{quantity_amount}\"\nquantity_unit = \"{quantity_unit}\"\nprice_amount = \"{price_amount}\"\nprice_currency = \"{price_currency}\"\nprice_per_amount = \"{price_per_amount}\"\nprice_per_unit = \"{price_per_unit}\"\nlabel = \"dozen\"\n\n[inventory]\navailable = \"{available}\"\n\n[availability]\nkind = \"status\"\nstatus = \"active\"\n\n[delivery]\nmethod = \"{delivery_method}\"\n\n[location]\nprimary = \"{location_primary}\"\n"
- )
-}
diff --git a/tests/local.rs b/tests/local.rs
@@ -1,164 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn local_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn local_init_json_creates_replica_db_and_roots() {
- let dir = tempdir().expect("tempdir");
- let output = local_command_in(dir.path())
- .args(["--json", "local", "init"])
- .output()
- .expect("run local init");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "initialized");
- assert_eq!(json["replica_db"], "ready");
-
- let replica_db = data_root(dir.path()).join("apps/cli/replica/replica.sqlite");
- assert!(replica_db.exists());
- assert!(
- data_root(dir.path())
- .join("apps/cli/replica/backups")
- .exists()
- );
- assert!(
- data_root(dir.path())
- .join("apps/cli/replica/exports")
- .exists()
- );
-}
-
-#[test]
-fn local_status_reports_unconfigured_when_replica_is_missing() {
- let dir = tempdir().expect("tempdir");
- let output = local_command_in(dir.path())
- .args(["--json", "local", "status"])
- .output()
- .expect("run local status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["replica_db"], "missing");
- assert_eq!(json["actions"][0], "radroots local init");
-}
-
-#[test]
-fn local_status_reports_real_replica_metadata_after_init() {
- let dir = tempdir().expect("tempdir");
- let init = local_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let output = local_command_in(dir.path())
- .args(["--json", "local", "status"])
- .output()
- .expect("run local status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["counts"]["farms"], 0);
- assert_eq!(json["counts"]["listings"], 0);
- assert_eq!(json["sync"]["expected_count"], 0);
- assert_eq!(json["sync"]["pending_count"], 0);
-}
-
-#[test]
-fn local_backup_and_export_write_files() {
- let dir = tempdir().expect("tempdir");
- let init = local_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let backup_path = dir.path().join("backup").join("local.radb");
- let backup = local_command_in(dir.path())
- .args([
- "--json",
- "local",
- "backup",
- "--output",
- backup_path.to_str().expect("backup path"),
- ])
- .output()
- .expect("run local backup");
- assert!(backup.status.success());
- let backup_json: Value = serde_json::from_slice(backup.stdout.as_slice()).expect("json");
- assert_eq!(backup_json["state"], "backup created");
- assert!(backup_path.exists());
- assert!(fs::metadata(&backup_path).expect("backup metadata").len() > 0);
-
- let export_path = dir.path().join("export").join("local.ndjson");
- let export = local_command_in(dir.path())
- .args([
- "--json",
- "local",
- "export",
- "--format",
- "ndjson",
- "--output",
- export_path.to_str().expect("export path"),
- ])
- .output()
- .expect("run local export");
- assert!(export.status.success());
- let export_json: Value = serde_json::from_slice(export.stdout.as_slice()).expect("json");
- assert_eq!(export_json["state"], "exported");
- assert_eq!(export_json["format"], "ndjson");
- let export_raw = fs::read_to_string(&export_path).expect("read export");
- let lines = export_raw.lines().collect::<Vec<_>>();
- assert!(lines.len() >= 3);
- assert!(lines[0].contains("\"kind\":\"local_export_manifest\""));
-}
diff --git a/tests/market.rs b/tests/market.rs
@@ -1,437 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use radroots_sql_core::{SqlExecutor, SqliteExecutor};
-use serde_json::{Value, json};
-use tempfile::tempdir;
-
-const ADDRESS_BACKED_LISTING_ADDR: &str =
- "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_HYF_ENABLED",
- "RADROOTS_HYF_EXECUTABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn seed_trade_product(
- workdir: &Path,
- product_id: &str,
- key: &str,
- listing_addr: Option<&str>,
- category: &str,
- title: &str,
- summary: &str,
- qty_amt: i64,
- qty_avail: i64,
- location_label: Option<&str>,
-) {
- let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
- let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
- let now = "2026-04-07T00:00:00.000Z";
- executor
- .exec(
- "INSERT INTO trade_product (id, created_at, updated_at, key, listing_addr, 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- product_id,
- now,
- now,
- key,
- listing_addr,
- category,
- title,
- summary,
- "fresh",
- "lot-a",
- "standard",
- 2026,
- qty_amt,
- "kg",
- "1 kg tomato lot",
- qty_avail,
- 10.0,
- "USD",
- 1,
- "kg",
- Value::Null
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert trade product");
-
- if let Some(location_label) = location_label {
- let location_id = format!("11111111-1111-1111-1111-{}", &product_id[24..]);
- executor
- .exec(
- "INSERT INTO gcs_location (id, created_at, updated_at, d_tag, lat, lng, geohash, point, polygon, accuracy, altitude, tag_0, label, area, elevation, soil, climate, gc_id, gc_name, gc_admin1_id, gc_admin1_name, gc_country_id, gc_country_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- location_id,
- now,
- now,
- format!("location-{product_id}"),
- 35.0,
- -82.0,
- "dnrj",
- "POINT(-82 35)",
- "POLYGON EMPTY",
- Value::Null,
- Value::Null,
- Value::Null,
- location_label,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- Value::Null,
- location_label,
- Value::Null,
- Value::Null,
- Value::Null,
- "USA"
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert gcs location");
- executor
- .exec(
- "INSERT INTO trade_product_location (tb_tp, tb_gl) VALUES (?, ?);",
- json!([product_id, location_id]).to_string().as_str(),
- )
- .expect("insert trade product location");
- }
-}
-
-fn write_fake_hyfd(
- workdir: &Path,
- status_response: &str,
- rewrite_response: &str,
-) -> std::path::PathBuf {
- let path = workdir.join("fake-hyfd");
- let script = format!(
- "#!/bin/sh\nread -r request || exit 64\ncase \"$request\" in\n *'\"capability\":\"sys.status\"'*)\n cat <<'JSON'\n{status_response}\nJSON\n ;;\n *'\"capability\":\"query_rewrite\"'*)\n cat <<'JSON'\n{rewrite_response}\nJSON\n ;;\n *)\n cat <<'JSON'\n{{\"version\":1,\"request_id\":\"unexpected\",\"ok\":false,\"error\":{{\"code\":\"unsupported_capability\",\"message\":\"unexpected request\"}}}}\nJSON\n ;;\nesac\n"
- );
- fs::write(&path, script).expect("write fake hyfd");
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let mut permissions = fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("chmod fake hyfd");
- }
- path
-}
-
-#[test]
-fn market_update_reports_missing_local_data_and_relay_setup() {
- let dir = tempdir().expect("tempdir");
- let output = cli_command_in(dir.path())
- .args(["market", "update"])
- .output()
- .expect("run market update");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Not ready yet"));
- assert!(stdout.contains("Missing"));
- assert!(stdout.contains("Local market data"));
- assert!(stdout.contains("Relay configuration"));
- assert!(stdout.contains("radroots local init"));
- assert!(stdout.contains("radroots relay list --relay wss://relay.example.com"));
-}
-
-#[test]
-fn market_update_stays_honest_about_unavailable_ingest() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let config_dir = dir.path().join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.one\"]\npublish_policy = \"any\"\n",
- )
- .expect("write user config");
-
- let json_output = cli_command_in(dir.path())
- .args(["--json", "market", "update"])
- .output()
- .expect("run market update json");
- assert_eq!(json_output.status.code(), Some(4));
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["direction"], "pull");
- assert_eq!(json["state"], "unavailable");
- assert_eq!(json["relay_count"], 1);
- assert_eq!(json["actions"][0], "radroots rpc status");
- assert_eq!(json["actions"][1], "radroots runtime status radrootsd");
- assert_eq!(json["actions"][2], "radroots sync status");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|reason| reason.contains("relay ingest"))
- );
-
- let human_output = cli_command_in(dir.path())
- .args(["market", "update"])
- .output()
- .expect("run market update human");
- assert_eq!(human_output.status.code(), Some(4));
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Unavailable right now"));
- assert!(stdout.contains("relay ingest is not wired into `radroots sync pull` yet"));
- assert!(stdout.contains("Next"));
- assert!(stdout.contains("radroots rpc status"));
-}
-
-#[test]
-fn market_search_preserves_machine_shape_and_renders_card_list() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000401",
- "sf-tomatoes",
- Some(ADDRESS_BACKED_LISTING_ADDR),
- "produce",
- "San Francisco Early Girl Tomatoes",
- "Fresh local tomatoes packed for pickup from the farm.",
- 18,
- 12,
- Some("San Francisco, CA"),
- );
-
- let json_output = cli_command_in(dir.path())
- .args(["--json", "market", "search", "tomatoes"])
- .output()
- .expect("run market search json");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["count"], 1);
- assert_eq!(json["results"][0]["product_key"], "sf-tomatoes");
- assert_eq!(
- json["results"][0]["listing_addr"],
- ADDRESS_BACKED_LISTING_ADDR
- );
- assert_eq!(
- json["results"][0]["title"],
- "San Francisco Early Girl Tomatoes"
- );
- assert_eq!(json["results"][0]["location_primary"], "San Francisco, CA");
- assert_eq!(json["actions"][0], "radroots market view sf-tomatoes");
- assert_eq!(
- json["actions"][1],
- "radroots order create --listing sf-tomatoes"
- );
-
- let ndjson_output = cli_command_in(dir.path())
- .args(["--ndjson", "market", "search", "tomatoes"])
- .output()
- .expect("run market search ndjson");
- assert!(ndjson_output.status.success());
- let stdout = String::from_utf8(ndjson_output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 1);
- assert!(lines[0].contains("\"product_key\":\"sf-tomatoes\""));
- assert!(lines[0].contains("\"title\":\"San Francisco Early Girl Tomatoes\""));
-
- let human_output = cli_command_in(dir.path())
- .args(["market", "search", "tomatoes"])
- .output()
- .expect("run market search human");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("1 listing for tomatoes"));
- assert!(stdout.contains("San Francisco Early Girl Tomatoes"));
- assert!(stdout.contains("Key"));
- assert!(stdout.contains("Place"));
- assert!(stdout.contains("Offer"));
- assert!(stdout.contains("Next"));
- assert!(stdout.contains("radroots market view sf-tomatoes"));
- assert!(stdout.contains("radroots order create --listing sf-tomatoes"));
- assert!(!stdout.contains("market ยท local first"));
-
- let quiet_output = cli_command_in(dir.path())
- .args(["--quiet", "market", "search", "tomatoes"])
- .output()
- .expect("run quiet market search");
- assert!(quiet_output.status.success());
- let quiet_stdout = String::from_utf8(quiet_output.stdout).expect("utf8 stdout");
- assert_eq!(quiet_stdout.trim(), "sf-tomatoes");
-}
-
-#[test]
-fn market_search_uses_also_searched_for_when_hyf_rewrites_query() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000402",
- "fresh-eggs",
- None,
- "protein",
- "Fresh Eggs",
- "Pasture-raised eggs",
- 36,
- 24,
- Some("Marshall"),
- );
-
- let hyfd = write_fake_hyfd(
- dir.path(),
- r#"{"version":1,"request_id":"cli-doctor-hyf-status","trace_id":"cli-doctor-hyf-status","ok":true,"output":{"build_identity":{"protocol_version":1},"enabled_execution_modes":{"deterministic":true}}}"#,
- r#"{"version":1,"request_id":"cli-find-query-rewrite","trace_id":"cli-find-query-rewrite","ok":true,"output":{"original_text":"henhouse","normalized_text":"henhouse","rewritten_text":"eggs","query_terms":["eggs"],"normalization_signals":["query_rewrite"],"ranking_hints":["local_first"],"extracted_filters":{"local_intent":false,"fulfillment":"any","time_window":"any"}}}"#,
- );
-
- let json_output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", &hyfd)
- .args(["--json", "market", "search", "henhouse"])
- .output()
- .expect("run market search json");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["hyf"]["state"], "query_rewrite_applied");
- assert_eq!(json["hyf"]["rewritten_query"], "eggs");
- assert_eq!(json["actions"][0], "radroots market view fresh-eggs");
- assert_eq!(json["actions"].as_array().expect("actions").len(), 1);
-
- let human_output = cli_command_in(dir.path())
- .env("RADROOTS_HYF_ENABLED", "true")
- .env("RADROOTS_HYF_EXECUTABLE", &hyfd)
- .args(["market", "search", "henhouse"])
- .output()
- .expect("run market search human");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("1 listing for eggs"));
- assert!(stdout.contains("Also searched for"));
- assert!(stdout.contains("henhouse"));
- assert!(stdout.contains("radroots market view fresh-eggs"));
- assert!(!stdout.contains("radroots order create --listing fresh-eggs"));
- assert!(!stdout.contains("hyf: query rewritten"));
-}
-
-#[test]
-fn market_view_wraps_listing_reads_and_guides_to_order_create() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000403",
- "pasture-eggs",
- Some(ADDRESS_BACKED_LISTING_ADDR),
- "protein",
- "Pasture Eggs",
- "Fresh pasture-raised eggs collected daily.",
- 36,
- 18,
- Some("Marshall"),
- );
-
- let json_output = cli_command_in(dir.path())
- .args(["--json", "market", "view", "pasture-eggs"])
- .output()
- .expect("run market view json");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["product_key"], "pasture-eggs");
- assert_eq!(json["listing_addr"], ADDRESS_BACKED_LISTING_ADDR);
- assert_eq!(json["title"], "Pasture Eggs");
- assert_eq!(json["location_primary"], "Marshall");
- assert_eq!(
- json["actions"][0],
- "radroots order create --listing pasture-eggs"
- );
-
- let human_output = cli_command_in(dir.path())
- .args(["market", "view", "pasture-eggs"])
- .output()
- .expect("run market view human");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Pasture Eggs"));
- assert!(stdout.contains("Listing"));
- assert!(stdout.contains("Key"));
- assert!(stdout.contains("Place"));
- assert!(stdout.contains("About"));
- assert!(stdout.contains("radroots order create --listing pasture-eggs"));
- assert!(!stdout.contains("listing ยท"));
-
- let missing_output = cli_command_in(dir.path())
- .args(["--json", "market", "view", "missing-listing"])
- .output()
- .expect("run missing market view");
- assert!(missing_output.status.success());
- let missing_json: Value =
- serde_json::from_slice(missing_output.stdout.as_slice()).expect("json");
- assert_eq!(missing_json["state"], "missing");
- assert_eq!(
- missing_json["actions"][0],
- "radroots market search tomatoes"
- );
- assert_eq!(missing_json["actions"][1], "radroots market update");
-}
diff --git a/tests/myc_status.rs b/tests/myc_status.rs
@@ -1,922 +0,0 @@
-use std::fs;
-use std::os::unix::fs::PermissionsExt;
-use std::path::Path;
-use std::process::Command;
-use std::sync::{Mutex, MutexGuard, OnceLock};
-
-use assert_cmd::prelude::*;
-use radroots_identity::RadrootsIdentity;
-use radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionId;
-use serde_json::{Value, json};
-use tempfile::tempdir;
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn write_user_config(workdir: &Path, contents: &str) {
- let config_dir = workdir.join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(config_dir.join("config.toml"), contents).expect("write user config");
-}
-
-#[test]
-fn myc_status_reports_ready_for_valid_signer_status_payload() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(sample_status_payload(true).to_string()).as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["source"], "myc status command ยท local first");
- assert_eq!(json["ready"], true);
- assert_eq!(json["service_status"], "healthy");
- assert_eq!(json["remote_session_count"], 1);
- assert_eq!(json["remote_sessions"][0]["permissions"][0], "sign_event");
- assert_eq!(json["local_signer"]["availability"], "secret_backed");
- assert_eq!(json["custody"]["signer"]["resolved"], true);
- assert_eq!(
- json["custody"]["user"]["selected_account_state"],
- "public_only"
- );
-}
-
-#[test]
-fn myc_status_reports_unavailable_for_invalid_status_payload() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(dir.path(), "#!/bin/sh\nprintf '%s\\n' 'this is not json'\n");
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- assert_eq!(json["ready"], false);
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("not valid JSON"))
- );
-}
-
-#[test]
-fn myc_status_rejects_missing_signer_status_contract_version() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let mut payload = sample_status_payload(true);
- payload
- .as_object_mut()
- .expect("payload object")
- .remove("status_contract_version");
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("contract version 1"))
- );
-}
-
-#[test]
-fn myc_status_rejects_incompatible_signer_status_contract_version() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let mut payload = sample_status_payload(true);
- payload["status_contract_version"] = json!(99);
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("contract version 99"))
- );
-}
-
-#[test]
-fn myc_status_reports_degraded_service_as_external_unavailable() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(sample_status_payload(false).to_string()).as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "degraded");
- assert_eq!(json["service_status"], "degraded");
- assert_eq!(json["ready"], false);
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("transport quorum is below target"))
- );
-}
-
-#[test]
-fn signer_status_reports_degraded_myc_backend_as_external_unavailable() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(sample_status_payload(false).to_string()).as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["mode"], "myc");
- assert_eq!(json["state"], "degraded");
- assert_eq!(json["source"], "myc status command ยท local first");
- assert_eq!(json["signer_account_id"], Value::Null);
- assert_eq!(json["myc"]["state"], "degraded");
- assert_eq!(json["myc"]["service_status"], "degraded");
- assert_eq!(json["binding"]["state"], "unconfigured");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("transport quorum is below target"))
- );
-}
-
-#[test]
-fn signer_status_reports_ready_for_configured_myc_managed_account_binding() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let payload = sample_status_payload(true);
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
- let managed_account_ref =
- payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"]
- .as_str()
- .expect("managed account ref");
- let provider_account_ref = payload["signer_backend"]["local_signer"]["account_id"]
- .as_str()
- .expect("provider account ref");
- assert_ne!(managed_account_ref, provider_account_ref);
- let signer_session_ref = payload["signer_backend"]["remote_sessions"][0]["connection_id"]
- .as_str()
- .expect("signer session ref");
- write_user_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{managed_account_ref}"
-signer_session_ref = "{signer_session_ref}"
-"#
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["mode"], "myc");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["source"], "user config [[capability_binding]]");
- assert_eq!(json["signer_account_id"], managed_account_ref);
- assert_eq!(json["binding"]["state"], "ready");
- assert_eq!(
- json["binding"]["resolved_signer_session_id"],
- signer_session_ref
- );
- assert_eq!(json["binding"]["managed_account_ref"], managed_account_ref);
- assert_eq!(json["binding"]["signer_session_ref"], signer_session_ref);
- assert_eq!(json["myc"]["remote_session_count"], 1);
- assert_eq!(
- json["myc"]["remote_sessions"][0]["user_identity"]["id"],
- managed_account_ref
- );
- assert_ne!(
- json["myc"]["remote_sessions"][0]["signer_identity"]["id"],
- json["myc"]["remote_sessions"][0]["user_identity"]["id"]
- );
- assert_ne!(
- json["myc"]["local_signer"]["account_id"],
- json["myc"]["remote_sessions"][0]["user_identity"]["id"]
- );
- assert!(
- json["write_kinds"]
- .as_array()
- .expect("write kinds")
- .iter()
- .all(|kind| kind["ready"] == true)
- );
-}
-
-#[test]
-fn signer_status_reports_kind_specific_myc_write_readiness() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let payload = sample_status_payload_with_permissions(true, &["sign_event:30402"]);
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
- let managed_account_ref =
- payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"]
- .as_str()
- .expect("managed account ref");
- let signer_session_ref = payload["signer_backend"]["remote_sessions"][0]["connection_id"]
- .as_str()
- .expect("signer session ref");
- write_user_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{managed_account_ref}"
-signer_session_ref = "{signer_session_ref}"
-"#
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["binding"]["state"], "ready");
- let write_kinds = json["write_kinds"].as_array().expect("write kinds");
- let listing = write_kinds
- .iter()
- .find(|kind| kind["event_kind"] == 30402)
- .expect("listing kind");
- assert_eq!(listing["command"], "listing publish");
- assert_eq!(listing["ready"], true);
- let order = write_kinds
- .iter()
- .find(|kind| kind["event_kind"] == 3422)
- .expect("order kind");
- assert_eq!(order["command"], "order submit");
- assert_eq!(order["ready"], false);
- assert!(
- order["reason"]
- .as_str()
- .is_some_and(|value| value.contains("sign_event:3422"))
- );
-}
-
-#[test]
-fn signer_status_reports_unconfigured_when_myc_binding_is_missing() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(sample_status_payload(true).to_string()).as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["binding"]["state"], "unconfigured");
- assert!(
- json["binding"]["reason"]
- .as_str()
- .is_some_and(|value| value.contains("configure [[capability_binding]]"))
- );
-}
-
-#[test]
-fn signer_status_reports_unsupported_for_explicit_endpoint_binding() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(sample_status_payload(true).to_string()).as_str(),
- );
- write_user_config(
- dir.path(),
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "explicit_endpoint"
-target = "https://myc.example.invalid"
-"#,
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["binding"]["state"], "unsupported");
- assert!(
- json["binding"]["reason"]
- .as_str()
- .is_some_and(|value| value.contains("only supports target_kind `managed_instance`"))
- );
-}
-
-#[test]
-fn signer_status_reports_ambiguous_for_accountless_myc_binding() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let payload = sample_multi_session_status_payload();
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
- write_user_config(
- dir.path(),
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-"#,
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["binding"]["state"], "ambiguous");
- assert_eq!(json["binding"]["matched_session_count"], 2);
-}
-
-#[test]
-fn signer_status_reports_unauthorized_for_session_without_sign_event_permission() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let payload = sample_status_payload_with_permissions(true, &["nip44_encrypt"]);
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
- let managed_account_ref =
- payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"]
- .as_str()
- .expect("managed account ref");
- let signer_session_ref = payload["signer_backend"]["remote_sessions"][0]["connection_id"]
- .as_str()
- .expect("signer session ref");
- write_user_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{managed_account_ref}"
-signer_session_ref = "{signer_session_ref}"
-"#
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["binding"]["state"], "unauthorized");
- assert!(
- json["binding"]["reason"]
- .as_str()
- .is_some_and(|value| value.contains("not approved for any cli write event kind"))
- );
-}
-
-#[test]
-fn signer_status_does_not_treat_sign_event_star_as_wildcard() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let payload = sample_status_payload_with_permissions(true, &["sign_event:*"]);
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
- let managed_account_ref =
- payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"]
- .as_str()
- .expect("managed account ref");
- let signer_session_ref = payload["signer_backend"]["remote_sessions"][0]["connection_id"]
- .as_str()
- .expect("signer session ref");
- write_user_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{managed_account_ref}"
-signer_session_ref = "{signer_session_ref}"
-"#
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["binding"]["state"], "unauthorized");
- assert!(
- json["write_kinds"]
- .as_array()
- .expect("write kinds")
- .iter()
- .all(|kind| kind["ready"] == false)
- );
-}
-
-#[test]
-fn signer_status_reports_unavailable_for_missing_bound_session() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let payload = sample_status_payload(true);
- let executable = write_fake_myc(
- dir.path(),
- successful_status_script(payload.to_string()).as_str(),
- );
- let managed_account_ref =
- payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"]
- .as_str()
- .expect("managed account ref");
- write_user_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{managed_account_ref}"
-signer_session_ref = "missing-session"
-"#
- )
- .as_str(),
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(4));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- assert_eq!(json["binding"]["state"], "unavailable");
- assert!(
- json["binding"]["reason"]
- .as_str()
- .is_some_and(|value| value.contains("missing-session"))
- );
-}
-
-#[test]
-fn myc_status_reports_unavailable_when_executable_is_missing() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let missing = dir.path().join("missing-myc");
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- missing.to_str().expect("missing path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("was not found"))
- );
-}
-
-#[test]
-fn myc_status_reports_unavailable_for_non_zero_exit() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- "#!/bin/sh\nprintf '%s\\n' 'transport unavailable' >&2\nexit 42\n",
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- let reason = json["reason"].as_str().expect("reason string");
- assert!(reason.contains("status code 42") || reason.contains("transport unavailable"));
-}
-
-#[test]
-fn myc_status_reports_unavailable_for_timeout() {
- let _guard = myc_test_guard();
- let dir = tempdir().expect("tempdir");
- let executable = write_fake_myc(
- dir.path(),
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"signer\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\nexec sleep 5\n",
- );
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--myc-status-timeout-ms",
- "25",
- "--myc-executable",
- executable.to_str().expect("executable path"),
- "myc",
- "status",
- ])
- .output()
- .expect("run myc status");
-
- assert_eq!(output.status.code(), Some(4));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["state"], "unavailable");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("timed out after 25ms"))
- );
-}
-
-fn write_fake_myc(dir: &std::path::Path, script: &str) -> std::path::PathBuf {
- let path = dir.join("fake-myc");
- fs::write(&path, script).expect("write fake myc");
- let mut permissions = fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("chmod fake myc");
- path
-}
-
-fn myc_test_guard() -> MutexGuard<'static, ()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(()))
- .lock()
- .expect("lock myc integration tests")
-}
-
-fn successful_status_script(payload_json: String) -> String {
- format!(
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"signer\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\ncat <<'JSON'\n{payload_json}\nJSON\n"
- )
-}
-
-fn sample_status_payload(ready: bool) -> Value {
- sample_status_payload_with_permissions(ready, &["sign_event"])
-}
-
-fn sample_status_payload_with_permissions(ready: bool, permissions: &[&str]) -> Value {
- let signer_identity = RadrootsIdentity::generate().to_public();
- let user_identity = RadrootsIdentity::generate().to_public();
- let session_id = RadrootsNostrSignerConnectionId::new_v7().to_string();
- let signer_id = signer_identity.id.clone();
- let signer_public_key_hex = signer_identity.public_key_hex.clone();
- let user_id = user_identity.id.clone();
- let user_public_key_hex = user_identity.public_key_hex.clone();
- let service_status = if ready { "healthy" } else { "degraded" };
- let reasons = if ready {
- Vec::<String>::new()
- } else {
- vec!["transport quorum is below target".to_owned()]
- };
-
- json!({
- "status_contract_version": 1,
- "status": service_status,
- "ready": ready,
- "reasons": reasons,
- "signer_backend": {
- "local_signer": {
- "account_id": signer_id,
- "public_identity": signer_identity.clone(),
- "availability": "SecretBacked"
- },
- "remote_session_count": 1,
- "remote_sessions": [
- {
- "connection_id": session_id,
- "signer_identity": signer_identity,
- "user_identity": user_identity.clone(),
- "relays": ["wss://relay.example.test"],
- "permissions": permissions.join(",")
- }
- ]
- },
- "custody": {
- "signer": {
- "resolved": true,
- "selected_account_id": "signer-account",
- "selected_account_state": "ready",
- "identity_id": signer_id,
- "public_key_hex": signer_public_key_hex
- },
- "user": {
- "resolved": true,
- "selected_account_id": "user-account",
- "selected_account_state": "public_only",
- "identity_id": user_id,
- "public_key_hex": user_public_key_hex
- }
- }
- })
-}
-
-fn sample_multi_session_status_payload() -> Value {
- let first_signer = RadrootsIdentity::generate().to_public();
- let first_user = RadrootsIdentity::generate().to_public();
- let second_signer = RadrootsIdentity::generate().to_public();
- let second_user = RadrootsIdentity::generate().to_public();
- let first_signer_id = first_signer.id.clone();
- let first_signer_public_key_hex = first_signer.public_key_hex.clone();
- let first_user_id = first_user.id.clone();
- let first_user_public_key_hex = first_user.public_key_hex.clone();
-
- json!({
- "status_contract_version": 1,
- "status": "healthy",
- "ready": true,
- "reasons": [],
- "signer_backend": {
- "local_signer": {
- "account_id": first_signer_id,
- "public_identity": first_signer.clone(),
- "availability": "SecretBacked"
- },
- "remote_session_count": 2,
- "remote_sessions": [
- {
- "connection_id": RadrootsNostrSignerConnectionId::new_v7().to_string(),
- "signer_identity": first_signer,
- "user_identity": first_user.clone(),
- "relays": ["wss://relay.example.test"],
- "permissions": "sign_event"
- },
- {
- "connection_id": RadrootsNostrSignerConnectionId::new_v7().to_string(),
- "signer_identity": second_signer,
- "user_identity": second_user,
- "relays": ["wss://relay-secondary.example.test"],
- "permissions": "sign_event"
- }
- ]
- },
- "custody": {
- "signer": {
- "resolved": true,
- "selected_account_id": "signer-account",
- "selected_account_state": "ready",
- "identity_id": first_signer_id,
- "public_key_hex": first_signer_public_key_hex
- },
- "user": {
- "resolved": true,
- "selected_account_id": "user-account",
- "selected_account_state": "public_only",
- "identity_id": first_user_id,
- "public_key_hex": first_user_public_key_hex
- }
- }
- })
-}
diff --git a/tests/order.rs b/tests/order.rs
@@ -1,1817 +0,0 @@
-use std::fs;
-use std::io::{Read, Write};
-use std::net::{TcpListener, TcpStream};
-use std::os::unix::fs::PermissionsExt;
-use std::path::Path;
-use std::process::Command;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
-use std::thread::{self, JoinHandle};
-use std::time::Duration;
-
-use assert_cmd::prelude::*;
-use radroots_identity::RadrootsIdentity;
-use radroots_sql_core::{SqlExecutor, SqliteExecutor};
-use serde_json::{Value, json};
-use tempfile::tempdir;
-
-const ORDER_SELLER_PUBKEY: &str =
- "1111111111111111111111111111111111111111111111111111111111111111";
-const ORDER_LISTING_ADDR: &str =
- "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-const ORDER_DRAFT_LISTING_ADDR: &str =
- "30403:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn order_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn write_user_config(workdir: &Path, contents: &str) {
- let config_dir = workdir.join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(config_dir.join("config.toml"), contents).expect("write user config");
-}
-
-fn init_local_replica(workdir: &Path) {
- let init = order_command_in(workdir)
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-}
-
-fn seed_trade_product(workdir: &Path, product_id: &str, key: &str, listing_addr: Option<&str>) {
- let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
- let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
- let now = "2026-04-07T00:00:00.000Z";
- executor
- .exec(
- "INSERT INTO trade_product (id, created_at, updated_at, key, listing_addr, 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
- json!([
- product_id,
- now,
- now,
- key,
- listing_addr,
- "produce",
- "Pasture Eggs",
- "Fresh pasture-raised eggs",
- "fresh",
- "lot-a",
- "standard",
- 2026,
- 36,
- "each",
- "dozen",
- 18,
- 4.5,
- "USD",
- 1,
- "each",
- Value::Null
- ])
- .to_string()
- .as_str(),
- )
- .expect("insert trade product");
-}
-
-fn assert_no_order_drafts(workdir: &Path) {
- let drafts_dir = data_root(workdir).join("apps/cli/orders/drafts");
- if !drafts_dir.exists() {
- return;
- }
- assert!(
- fs::read_dir(&drafts_dir)
- .expect("read drafts dir")
- .next()
- .is_none()
- );
-}
-
-fn run_order_lookup_failure(seed: impl FnOnce(&Path), expected_stderr: &str) {
- let dir = tempdir().expect("tempdir");
- seed(dir.path());
-
- let output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--bin",
- "bin-1",
- "--qty",
- "1",
- ])
- .output()
- .expect("run order new failure");
- assert_eq!(output.status.code(), Some(2));
- let stderr = String::from_utf8(output.stderr).expect("stderr utf8");
- assert!(
- stderr.contains(expected_stderr),
- "stderr did not contain `{expected_stderr}`: {stderr}"
- );
- assert_no_order_drafts(dir.path());
-}
-
-fn config_with_write_plane(extra: &str, url: &str) -> String {
- let mut rendered = String::new();
- if !extra.trim().is_empty() {
- rendered.push_str(extra.trim());
- rendered.push_str("\n\n");
- }
- rendered.push_str(
- format!(
- r#"[[capability_binding]]
-capability = "write_plane.trade_jsonrpc"
-provider = "radrootsd"
-target_kind = "explicit_endpoint"
-target = "{url}"
-"#
- )
- .as_str(),
- );
- rendered
-}
-
-fn write_fake_myc(dir: &Path, script: &str) -> std::path::PathBuf {
- let path = dir.join("fake-myc");
- fs::write(&path, script).expect("write fake myc");
- let mut permissions = fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("chmod fake myc");
- path
-}
-
-fn successful_status_script(payload_json: String) -> String {
- format!(
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"signer\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\ncat <<'JSON'\n{payload_json}\nJSON\n"
- )
-}
-
-fn sample_myc_status_payload(
- account_id: &str,
- public_identity: &Value,
- connection_id: &str,
-) -> Value {
- let signer_identity =
- serde_json::to_value(RadrootsIdentity::generate().to_public()).expect("signer identity");
- let signer_account_id = signer_identity["id"]
- .as_str()
- .expect("signer id")
- .to_owned();
- assert_ne!(signer_account_id, account_id);
- json!({
- "status_contract_version": 1,
- "status": "healthy",
- "ready": true,
- "reasons": [],
- "signer_backend": {
- "local_signer": {
- "account_id": signer_account_id,
- "public_identity": signer_identity.clone(),
- "availability": "SecretBacked"
- },
- "remote_session_count": 1,
- "remote_sessions": [
- {
- "connection_id": connection_id,
- "signer_identity": signer_identity,
- "user_identity": public_identity,
- "relays": ["wss://relay.one"],
- "permissions": "sign_event"
- }
- ]
- }
- })
-}
-
-fn order_test_guard() -> MutexGuard<'static, ()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(()))
- .lock()
- .expect("order test lock")
-}
-
-#[derive(Debug, Clone)]
-struct MockRpcRequest {
- body: Value,
- method: String,
- auth_header: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-struct MockRpcResponse {
- body: Value,
-}
-
-impl MockRpcResponse {
- fn success(result: Value) -> Self {
- Self {
- body: serde_json::json!({
- "jsonrpc": "2.0",
- "id": 1,
- "result": result,
- }),
- }
- }
-}
-
-struct MockRpcServer {
- address: String,
- shutdown: Arc<AtomicBool>,
- handle: Option<JoinHandle<()>>,
-}
-
-impl MockRpcServer {
- fn start<F>(handler: F) -> Self
- where
- F: Fn(Value, Option<String>) -> MockRpcResponse + Send + Sync + 'static,
- {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock rpc listener");
- listener
- .set_nonblocking(true)
- .expect("set mock rpc listener nonblocking");
- let address = listener
- .local_addr()
- .expect("mock rpc local addr")
- .to_string();
- let shutdown = Arc::new(AtomicBool::new(false));
- let shutdown_flag = Arc::clone(&shutdown);
- let handler: Arc<dyn Fn(Value, Option<String>) -> MockRpcResponse + Send + Sync> =
- Arc::new(handler);
- let handle = thread::spawn(move || {
- while !shutdown_flag.load(Ordering::SeqCst) {
- match listener.accept() {
- Ok((mut stream, _)) => {
- let _ = stream.set_nonblocking(false);
- if let Ok(request) = read_request(&mut stream) {
- let response =
- handler(request.body.clone(), request.auth_header.clone());
- let _ = write_response(&mut stream, &response);
- }
- }
- Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
- thread::sleep(Duration::from_millis(10));
- }
- Err(_) => {
- thread::sleep(Duration::from_millis(10));
- }
- }
- }
- });
-
- Self {
- address,
- shutdown,
- handle: Some(handle),
- }
- }
-
- fn url(&self) -> String {
- format!("http://{}", self.address)
- }
-}
-
-impl Drop for MockRpcServer {
- fn drop(&mut self) {
- self.shutdown.store(true, Ordering::SeqCst);
- let _ = TcpStream::connect(&self.address);
- if let Some(handle) = self.handle.take() {
- handle.join().expect("join mock rpc thread");
- }
- }
-}
-
-fn read_request(stream: &mut TcpStream) -> Result<MockRpcRequest, String> {
- stream
- .set_read_timeout(Some(Duration::from_secs(2)))
- .map_err(|error| format!("set mock rpc read timeout: {error}"))?;
-
- let mut buffer = Vec::new();
- let mut chunk = [0_u8; 4096];
- let mut header_end = None;
- let mut content_length = 0_usize;
-
- loop {
- let read = stream
- .read(&mut chunk)
- .map_err(|error| format!("read mock rpc request: {error}"))?;
- if read == 0 {
- break;
- }
- buffer.extend_from_slice(&chunk[..read]);
- if header_end.is_none() {
- header_end = find_subslice(&buffer, b"\r\n\r\n").map(|index| index + 4);
- if let Some(end) = header_end {
- content_length = parse_content_length(&buffer[..end])?;
- if buffer.len() >= end + content_length {
- break;
- }
- }
- } else if let Some(end) = header_end {
- if buffer.len() >= end + content_length {
- break;
- }
- }
- }
-
- let end = header_end.ok_or_else(|| "mock rpc request did not include headers".to_owned())?;
- let headers = std::str::from_utf8(&buffer[..end])
- .map_err(|error| format!("mock rpc headers were not utf-8: {error}"))?;
- let auth_header = parse_header(headers, "authorization");
- let body = std::str::from_utf8(&buffer[end..end + content_length])
- .map_err(|error| format!("mock rpc body was not utf-8: {error}"))?;
- let envelope: Value =
- serde_json::from_str(body).map_err(|error| format!("parse mock rpc body: {error}"))?;
- let method = envelope["method"]
- .as_str()
- .ok_or_else(|| "mock rpc body did not include method".to_owned())?
- .to_owned();
-
- Ok(MockRpcRequest {
- body: envelope,
- method,
- auth_header,
- })
-}
-
-fn parse_content_length(headers: &[u8]) -> Result<usize, String> {
- let text =
- std::str::from_utf8(headers).map_err(|error| format!("parse mock rpc headers: {error}"))?;
- for line in text.lines() {
- if let Some((name, value)) = line.split_once(':') {
- if name.trim().eq_ignore_ascii_case("content-length") {
- return value
- .trim()
- .parse::<usize>()
- .map_err(|error| format!("parse content-length: {error}"));
- }
- }
- }
- Ok(0)
-}
-
-fn parse_header(headers: &str, wanted: &str) -> Option<String> {
- headers.lines().find_map(|line| {
- let (name, value) = line.split_once(':')?;
- if name.trim().eq_ignore_ascii_case(wanted) {
- Some(value.trim().to_owned())
- } else {
- None
- }
- })
-}
-
-fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
- haystack
- .windows(needle.len())
- .position(|window| window == needle)
-}
-
-fn write_response(stream: &mut TcpStream, response: &MockRpcResponse) -> Result<(), String> {
- let body = response.body.to_string();
- write!(
- stream,
- "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- )
- .map_err(|error| format!("write mock rpc response: {error}"))?;
- stream
- .flush()
- .map_err(|error| format!("flush mock rpc response: {error}"))
-}
-
-fn sample_bridge_job(job_id: &str, state: &str, terminal: bool, signer_session_id: &str) -> Value {
- serde_json::json!({
- "job_id": job_id,
- "command": "bridge.order.request",
- "idempotency_key": "order-submit-1",
- "status": state,
- "terminal": terminal,
- "recovered_after_restart": false,
- "requested_at_unix": 1_712_720_000,
- "completed_at_unix": terminal.then_some(1_712_720_030),
- "signer_mode": "nip46_session",
- "signer_session_id": signer_session_id,
- "event_kind": 30420,
- "event_id": "evt_order_01",
- "event_addr": "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "delivery_policy": "best_effort",
- "delivery_quorum": 2,
- "relay_count": 2,
- "acknowledged_relay_count": if terminal { 2 } else { 1 },
- "required_acknowledged_relay_count": 2,
- "attempt_count": if terminal { 2 } else { 1 },
- "relay_outcome_summary": if terminal { "submitted to 2 relays" } else { "awaiting relay quorum" },
- "attempt_summaries": if terminal {
- serde_json::json!(["attempt 1: relay.one accepted", "attempt 2: relay.two accepted"])
- } else {
- serde_json::json!(["attempt 1: relay.one accepted"])
- }
- })
-}
-
-#[test]
-fn order_new_creates_a_local_draft_with_selected_account_defaults() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
- let buyer_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey");
-
- init_local_replica(dir.path());
- seed_trade_product(
- dir.path(),
- "00000000-0000-0000-0000-000000000901",
- "pasture-eggs",
- Some(ORDER_LISTING_ADDR),
- );
-
- let output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--bin",
- "bin-1",
- "--qty",
- "2",
- ])
- .output()
- .expect("run order new");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("order json");
- assert_eq!(json["state"], "draft_created");
- assert_eq!(json["buyer_account_id"], account_id);
- assert_eq!(json["buyer_pubkey"], buyer_pubkey);
- assert_eq!(json["listing_addr"], ORDER_LISTING_ADDR);
- assert_eq!(json["seller_pubkey"], ORDER_SELLER_PUBKEY);
- assert_eq!(json["ready_for_submit"], true);
- assert_eq!(json["items"][0]["bin_id"], "bin-1");
- assert_eq!(json["items"][0]["bin_count"], 2);
-
- let file = json["file"].as_str().expect("draft file");
- assert!(file.contains("/data/apps/cli/orders/drafts/ord_"));
- let contents = fs::read_to_string(file).expect("read order draft");
- assert!(contents.contains("kind = \"order_draft_v1\""));
- assert!(contents.contains("listing_lookup = \"pasture-eggs\""));
- assert!(contents.contains(&format!("listing_addr = \"{ORDER_LISTING_ADDR}\"")));
- assert!(contents.contains(&format!("seller_pubkey = \"{ORDER_SELLER_PUBKEY}\"")));
- assert!(contents.contains(&format!("buyer_account_id = \"{account_id}\"")));
-}
-
-#[test]
-fn order_new_listing_lookup_failures_do_not_create_drafts() {
- let _guard = order_test_guard();
-
- run_order_lookup_failure(|_| {}, "requires local market data");
- run_order_lookup_failure(
- |workdir| {
- init_local_replica(workdir);
- },
- "is not available in the local replica",
- );
- run_order_lookup_failure(
- |workdir| {
- init_local_replica(workdir);
- seed_trade_product(
- workdir,
- "00000000-0000-0000-0000-000000000902",
- "pasture-eggs",
- None,
- );
- },
- "is missing a canonical listing address",
- );
- run_order_lookup_failure(
- |workdir| {
- init_local_replica(workdir);
- seed_trade_product(
- workdir,
- "00000000-0000-0000-0000-000000000903",
- "pasture-eggs",
- Some(ORDER_DRAFT_LISTING_ADDR),
- );
- },
- "must reference a public NIP-99 listing",
- );
- run_order_lookup_failure(
- |workdir| {
- init_local_replica(workdir);
- seed_trade_product(
- workdir,
- "00000000-0000-0000-0000-000000000904",
- "pasture-eggs",
- Some(ORDER_LISTING_ADDR),
- );
- seed_trade_product(
- workdir,
- "00000000-0000-0000-0000-000000000905",
- "pasture-eggs",
- Some(ORDER_LISTING_ADDR),
- );
- },
- "matched 2 local listings",
- );
-}
-
-#[test]
-fn order_get_and_ls_read_local_drafts_and_report_missing() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let first = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- ])
- .output()
- .expect("run first order new");
- assert!(first.status.success());
- let first_json: Value = serde_json::from_slice(first.stdout.as_slice()).expect("first json");
- let first_order_id = first_json["order_id"].as_str().expect("first order id");
-
- let second = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "carrots",
- "--listing-addr",
- ORDER_LISTING_ADDR,
- ])
- .output()
- .expect("run second order new");
- assert!(second.status.success());
- let second_json: Value = serde_json::from_slice(second.stdout.as_slice()).expect("second json");
- let second_order_id = second_json["order_id"].as_str().expect("second order id");
-
- let get_output = order_command_in(dir.path())
- .args(["--json", "order", "get", first_order_id])
- .output()
- .expect("run order get");
- assert!(get_output.status.success());
- let get_json: Value = serde_json::from_slice(get_output.stdout.as_slice()).expect("get json");
- assert_eq!(get_json["state"], "ready");
- assert_eq!(get_json["order_id"], first_order_id);
- assert_eq!(get_json["listing_lookup"], "pasture-eggs");
-
- let missing_output = order_command_in(dir.path())
- .args(["--json", "order", "get", "ord_missing"])
- .output()
- .expect("run missing order get");
- assert!(missing_output.status.success());
- let missing_json: Value =
- serde_json::from_slice(missing_output.stdout.as_slice()).expect("missing json");
- assert_eq!(missing_json["state"], "missing");
-
- let human_list = order_command_in(dir.path())
- .args(["order", "ls"])
- .output()
- .expect("run human order ls");
- assert!(human_list.status.success());
- let human_text = String::from_utf8(human_list.stdout).expect("human text");
- assert!(human_text.contains("orders ยท 2 local drafts"));
- assert!(human_text.contains(first_order_id));
- assert!(human_text.contains(second_order_id));
-
- let ndjson_output = order_command_in(dir.path())
- .args(["--ndjson", "order", "ls"])
- .output()
- .expect("run ndjson order ls");
- assert!(ndjson_output.status.success());
- let ndjson = String::from_utf8(ndjson_output.stdout).expect("ndjson text");
- let lines = ndjson.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines.iter().any(|line| line.contains(first_order_id)));
- assert!(lines.iter().any(|line| line.contains(second_order_id)));
-}
-
-#[test]
-fn order_create_view_and_list_aliases_wrap_the_existing_draft_surfaces() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let create_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "create",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- "--qty",
- "2",
- ])
- .output()
- .expect("run order create");
- assert!(create_output.status.success());
- let create_json: Value =
- serde_json::from_slice(create_output.stdout.as_slice()).expect("create json");
- let order_id = create_json["order_id"].as_str().expect("order id");
- assert_eq!(create_json["state"], "draft_created");
- assert_eq!(
- create_json["actions"][0],
- format!("radroots order view {order_id}")
- );
-
- let view_output = order_command_in(dir.path())
- .args(["--json", "order", "view", order_id])
- .output()
- .expect("run order view");
- assert!(view_output.status.success());
- let view_json: Value =
- serde_json::from_slice(view_output.stdout.as_slice()).expect("view json");
- assert_eq!(view_json["state"], "ready");
- assert_eq!(view_json["order_id"], order_id);
-
- let list_output = order_command_in(dir.path())
- .args(["--json", "order", "list"])
- .output()
- .expect("run order list");
- assert!(list_output.status.success());
- let list_json: Value =
- serde_json::from_slice(list_output.stdout.as_slice()).expect("list json");
- assert_eq!(list_json["count"], 1);
- assert_eq!(list_json["orders"][0]["id"], order_id);
-}
-
-#[test]
-fn order_list_empty_prefers_the_create_follow_up() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let output = order_command_in(dir.path())
- .args(["--json", "order", "list"])
- .output()
- .expect("run empty order list");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("list json");
- assert_eq!(json["state"], "empty");
- assert_eq!(json["actions"][0], "radroots order create");
-}
-
-#[test]
-fn order_get_surfaces_recorded_job_metadata_from_the_local_draft_store() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
- fs::create_dir_all(&drafts_dir).expect("create drafts dir");
- let draft_path = drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml");
- fs::write(
- &draft_path,
- r#"version = 1
-kind = "order_draft_v1"
-listing_lookup = "fresh-eggs"
-buyer_account_id = "acct_demo"
-
-[order]
-order_id = "ord_AAAAAAAAAAAAAAAAAAAAAg"
-listing_addr = "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg"
-buyer_pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-seller_pubkey = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
-
-[[order.items]]
-bin_id = "bin-1"
-bin_count = 2
-
-[submission]
-job_id = "job_order_01"
-"#,
- )
- .expect("write order draft");
-
- let output = order_command_in(dir.path())
- .args(["--json", "order", "get", "ord_AAAAAAAAAAAAAAAAAAAAAg"])
- .output()
- .expect("run order get");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "submitted");
- assert_eq!(json["job"]["job_id"], "job_order_01");
- assert_eq!(json["job"]["state"], "recorded");
- assert_eq!(json["ready_for_submit"], false);
-}
-
-#[test]
-fn order_submit_persists_submission_metadata_and_reports_job() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let new_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- ])
- .output()
- .expect("run order new");
- assert!(new_output.status.success());
- let new_json: Value = serde_json::from_slice(new_output.stdout.as_slice()).expect("new json");
- let order_id = new_json["order_id"].as_str().expect("order id");
- let file = new_json["file"].as_str().expect("file");
- let buyer_pubkey = new_json["buyer_pubkey"]
- .as_str()
- .expect("buyer pubkey")
- .to_owned();
-
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, auth_header| {
- recorded
- .lock()
- .expect("recorded requests lock")
- .push(MockRpcRequest {
- body: body.clone(),
- method: body["method"].as_str().unwrap_or_default().to_owned(),
- auth_header,
- });
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_order_01",
- buyer_pubkey.as_str(),
- &["sign_event"],
- true
- )])),
- "bridge.order.request" => MockRpcResponse::success(serde_json::json!({
- "deduplicated": false,
- "job": sample_bridge_job("job_order_01", "accepted", false, "sess_order_01"),
- })),
- "bridge.job.status" => MockRpcResponse::success(sample_bridge_job(
- "job_order_01",
- "accepted",
- false,
- "sess_order_01",
- )),
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
- .args([
- "--json",
- "order",
- "submit",
- order_id,
- "--idempotency-key",
- "order-submit-1",
- "--signer-session-id",
- "sess_order_01",
- ])
- .output()
- .expect("run order submit");
- assert!(submit_output.status.success());
- let submit_json: Value =
- serde_json::from_slice(submit_output.stdout.as_slice()).expect("submit json");
- assert_eq!(submit_json["state"], "accepted");
- assert_eq!(submit_json["signer_mode"], "nip46_session");
- assert_eq!(submit_json["signer_session_id"], "sess_order_01");
- assert_eq!(submit_json["job"]["job_id"], "job_order_01");
- assert_eq!(submit_json["job"]["command"], "order.submit");
- assert_eq!(submit_json["job"]["signer_mode"], "nip46_session");
- assert_eq!(submit_json["job"]["signer_session_id"], "sess_order_01");
- assert_eq!(submit_json["requested_signer_session_id"], "sess_order_01");
- assert_eq!(
- submit_json["job"]["requested_signer_session_id"],
- "sess_order_01"
- );
-
- let contents = fs::read_to_string(file).expect("read updated order draft");
- assert!(contents.contains("job_id = \"job_order_01\""));
- assert!(contents.contains("state = \"accepted\""));
- assert!(contents.contains("command = \"order.submit\""));
-
- let recorded_requests = requests.lock().expect("requests lock");
- assert!(
- recorded_requests
- .iter()
- .any(|request| request.method == "bridge.order.request")
- );
- assert!(
- recorded_requests
- .iter()
- .any(|request| request.method == "nip46.session.list")
- );
- let request = recorded_requests
- .iter()
- .find(|request| request.method == "bridge.order.request")
- .expect("bridge order request");
- assert_eq!(request.body["params"]["signer_session_id"], "sess_order_01");
- assert!(
- recorded_requests
- .iter()
- .any(|request| { request.auth_header.as_deref() == Some("Bearer test-token") })
- );
-}
-
-#[test]
-fn order_submit_quiet_reports_submitted_order_id() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let new_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- ])
- .output()
- .expect("run order new");
- assert!(new_output.status.success());
- let new_json: Value = serde_json::from_slice(new_output.stdout.as_slice()).expect("new json");
- let order_id = new_json["order_id"].as_str().expect("order id");
- let buyer_pubkey = new_json["buyer_pubkey"]
- .as_str()
- .expect("buyer pubkey")
- .to_owned();
-
- let server = MockRpcServer::start(move |body, _auth_header| {
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_order_quiet_01",
- buyer_pubkey.as_str(),
- &["sign_event"],
- true
- )])),
- "bridge.order.request" => MockRpcResponse::success(serde_json::json!({
- "deduplicated": false,
- "job": sample_bridge_job("job_order_quiet_01", "accepted", false, "sess_order_quiet_01"),
- })),
- "bridge.job.status" => MockRpcResponse::success(sample_bridge_job(
- "job_order_quiet_01",
- "accepted",
- false,
- "sess_order_quiet_01",
- )),
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "quiet-token")
- .args([
- "--quiet",
- "order",
- "submit",
- order_id,
- "--signer-session-id",
- "sess_order_quiet_01",
- ])
- .output()
- .expect("run quiet order submit");
- assert!(submit_output.status.success());
- let stdout = String::from_utf8(submit_output.stdout).expect("utf8 stdout");
- assert_eq!(stdout.trim(), format!("Order submitted: {order_id}"));
-}
-
-#[test]
-fn order_submit_watch_rejects_json_output() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let output = order_command_in(dir.path())
- .args(["--json", "order", "submit", "ord_demo", "--watch"])
- .output()
- .expect("run order submit watch json");
- assert_eq!(output.status.code(), Some(2));
- let stderr = String::from_utf8(output.stderr).expect("stderr utf8");
- assert!(stderr.contains("`order submit --watch` only supports human output"));
-}
-
-#[test]
-fn order_submit_watch_appends_human_watch_snapshots() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let buyer_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey")
- .to_owned();
-
- let create_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "create",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- ])
- .output()
- .expect("run order create");
- assert!(create_output.status.success());
- let create_json: Value =
- serde_json::from_slice(create_output.stdout.as_slice()).expect("create json");
- let order_id = create_json["order_id"].as_str().expect("order id");
-
- let server = MockRpcServer::start(move |body, _auth_header| {
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_order_watch_01",
- buyer_pubkey.as_str(),
- &["sign_event"],
- true
- )])),
- "bridge.order.request" => MockRpcResponse::success(serde_json::json!({
- "deduplicated": false,
- "job": sample_bridge_job(
- "job_order_watch_01",
- "accepted",
- false,
- "sess_order_watch_01"
- ),
- })),
- "bridge.job.status" => MockRpcResponse::success(sample_bridge_job(
- "job_order_watch_01",
- "completed",
- true,
- "sess_order_watch_01",
- )),
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "watch-token")
- .args([
- "order",
- "submit",
- order_id,
- "--watch",
- "--signer-session-id",
- "sess_order_watch_01",
- ])
- .output()
- .expect("run order submit watch");
- let stdout = String::from_utf8(output.stdout).expect("stdout utf8");
- let stderr = String::from_utf8(output.stderr).expect("stderr utf8");
- assert!(
- output.status.success(),
- "status: {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
- output.status.code()
- );
- assert!(stdout.contains("Order submitted"));
- assert!(stdout.contains("Watching order"));
- assert!(stdout.contains(order_id));
- assert!(stdout.contains("Completed"));
- assert!(stdout.contains("submitted to 2 relays"));
- assert!(!stdout.contains("order ยท"));
-}
-
-#[test]
-fn order_watch_reports_job_frames_for_submitted_order() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let polls = Arc::new(Mutex::new(0usize));
- let watch_polls = Arc::clone(&polls);
- let server = MockRpcServer::start(move |body, _auth_header| {
- match body["method"].as_str().unwrap_or_default() {
- "bridge.job.status" => {
- let mut count = watch_polls.lock().expect("watch polls lock");
- *count += 1;
- if *count == 1 {
- MockRpcResponse::success(sample_bridge_job(
- "job_watch_01",
- "accepted",
- false,
- "sess_order_01",
- ))
- } else {
- MockRpcResponse::success(sample_bridge_job(
- "job_watch_01",
- "completed",
- true,
- "sess_order_01",
- ))
- }
- }
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
-
- let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
- fs::create_dir_all(&drafts_dir).expect("create drafts dir");
- fs::write(
- drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml"),
- r#"version = 1
-kind = "order_draft_v1"
-listing_lookup = "fresh-eggs"
-buyer_account_id = "acct_demo"
-
-[order]
-order_id = "ord_AAAAAAAAAAAAAAAAAAAAAg"
-listing_addr = "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg"
-buyer_pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-seller_pubkey = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
-
-[[order.items]]
-bin_id = "bin-1"
-bin_count = 2
-
-[submission]
-job_id = "job_watch_01"
-"#,
- )
- .expect("write watch draft");
-
- let output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "watch-token")
- .args([
- "--json",
- "order",
- "watch",
- "ord_AAAAAAAAAAAAAAAAAAAAAg",
- "--frames",
- "2",
- "--interval-ms",
- "1",
- ])
- .output()
- .expect("run order watch");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("watch json");
- assert_eq!(json["state"], "completed");
- assert_eq!(json["frames"].as_array().map(Vec::len), Some(2));
- assert_eq!(json["frames"][0]["state"], "accepted");
- assert_eq!(json["frames"][1]["state"], "completed");
- assert_eq!(json["frames"][0]["signer_mode"], "nip46_session");
- assert_eq!(json["frames"][0]["signer_session_id"], "sess_order_01");
- assert_eq!(json["frames"][1]["signer_mode"], "nip46_session");
- assert_eq!(json["frames"][1]["signer_session_id"], "sess_order_01");
-
- let human_polls = Arc::new(Mutex::new(0usize));
- let human_watch_polls = Arc::clone(&human_polls);
- let human_server = MockRpcServer::start(move |body, _auth_header| {
- match body["method"].as_str().unwrap_or_default() {
- "bridge.job.status" => {
- let mut count = human_watch_polls.lock().expect("watch polls lock");
- *count += 1;
- if *count == 1 {
- MockRpcResponse::success(sample_bridge_job(
- "job_watch_01",
- "accepted",
- false,
- "sess_order_01",
- ))
- } else {
- MockRpcResponse::success(sample_bridge_job(
- "job_watch_01",
- "completed",
- true,
- "sess_order_01",
- ))
- }
- }
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
-
- let human_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", human_server.url())
- .env("RADROOTS_RPC_BEARER_TOKEN", "watch-token")
- .args([
- "order",
- "watch",
- "ord_AAAAAAAAAAAAAAAAAAAAAg",
- "--frames",
- "2",
- "--interval-ms",
- "1",
- ])
- .output()
- .expect("run human order watch");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Watching order ord_AAAAAAAAAAAAAAAAAAAAAg"));
- assert!(stdout.contains("Accepted"));
- assert!(stdout.contains("Completed"));
- assert!(stdout.contains("Summary"));
- assert!(!stdout.contains("order ยท"));
- assert!(!stdout.contains("\u{1b}"));
-}
-
-#[test]
-fn order_submit_uses_myc_binding_before_resolving_daemon_signer_session() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let account_id = account_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
- let public_identity = account_json["public_identity"].clone();
- let buyer_pubkey = public_identity["public_key_hex"]
- .as_str()
- .expect("buyer pubkey")
- .to_owned();
-
- let myc = write_fake_myc(
- dir.path(),
- successful_status_script(
- sample_myc_status_payload(
- account_id.as_str(),
- &public_identity,
- "conn_order_binding_01",
- )
- .to_string(),
- )
- .as_str(),
- );
-
- let new_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- "--qty",
- "2",
- ])
- .output()
- .expect("run order new");
- assert!(new_output.status.success());
- let new_json: Value = serde_json::from_slice(new_output.stdout.as_slice()).expect("new json");
- let order_id = new_json["order_id"].as_str().expect("order id");
-
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let session_account_id = account_id.clone();
- let provider_pubkey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
- let server = MockRpcServer::start(move |body, auth_header| {
- recorded
- .lock()
- .expect("recorded requests lock")
- .push(MockRpcRequest {
- body: body.clone(),
- method: body["method"].as_str().unwrap_or_default().to_owned(),
- auth_header: auth_header.clone(),
- });
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => {
- let mut session = sample_session_with_authority(
- "sess_order_02",
- provider_pubkey,
- &["sign_event"],
- true,
- Some(session_account_id.as_str()),
- Some("conn_order_binding_01"),
- );
- session["user_pubkey"] = Value::Null;
- MockRpcResponse::success(json!([session]))
- }
- "nip46.get_public_key" => {
- assert_eq!(auth_header.as_deref(), Some("Bearer test-token"));
- assert_eq!(body["params"]["session_id"], "sess_order_02");
- MockRpcResponse::success(json!({
- "pubkey": buyer_pubkey.as_str()
- }))
- }
- "bridge.order.request" => MockRpcResponse::success(serde_json::json!({
- "deduplicated": false,
- "job": sample_bridge_job("job_order_02", "accepted", false, "sess_order_02"),
- })),
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane(
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{account_id}"
-"#
- )
- .as_str(),
- server.url().as_str(),
- )
- .as_str(),
- );
-
- let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- myc.to_str().expect("myc path"),
- "order",
- "submit",
- order_id,
- ])
- .output()
- .expect("run order submit");
-
- let stdout = String::from_utf8(submit_output.stdout.clone()).expect("stdout utf8");
- let stderr = String::from_utf8(submit_output.stderr.clone()).expect("stderr utf8");
- assert!(
- submit_output.status.success(),
- "status: {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
- submit_output.status.code()
- );
- let submit_json: Value =
- serde_json::from_slice(submit_output.stdout.as_slice()).expect("submit json");
- assert_eq!(submit_json["state"], "accepted");
- assert_eq!(submit_json["signer_mode"], "nip46_session");
- assert_eq!(submit_json["signer_session_id"], "sess_order_02");
- assert_eq!(submit_json["requested_signer_session_id"], Value::Null);
-
- let recorded_requests = requests.lock().expect("requests lock");
- assert!(
- recorded_requests
- .iter()
- .any(|request| request.method == "nip46.session.list")
- );
- let request = recorded_requests
- .iter()
- .find(|request| request.method == "bridge.order.request")
- .expect("bridge order request");
- assert!(
- recorded_requests
- .iter()
- .any(|request| request.method == "nip46.get_public_key")
- );
- assert_eq!(request.body["params"]["signer_session_id"], "sess_order_02");
- assert_eq!(
- request.body["params"]["signer_authority"]["provider_runtime_id"],
- "myc"
- );
- assert_eq!(
- request.body["params"]["signer_authority"]["account_identity_id"],
- account_id
- );
- assert_eq!(
- request.body["params"]["signer_authority"]["provider_signer_session_id"],
- "conn_order_binding_01"
- );
-}
-
-#[test]
-fn order_submit_rejects_myc_binding_that_resolves_the_wrong_actor() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let new_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- "--qty",
- "2",
- ])
- .output()
- .expect("run order new");
- assert!(new_output.status.success());
- let new_json: Value = serde_json::from_slice(new_output.stdout.as_slice()).expect("new json");
- let order_id = new_json["order_id"].as_str().expect("order id");
-
- let mismatch_account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run mismatch account new");
- assert!(mismatch_account_output.status.success());
- let mismatch_account_json: Value =
- serde_json::from_slice(mismatch_account_output.stdout.as_slice()).expect("mismatch json");
- let mismatch_account_id = mismatch_account_json["account"]["id"]
- .as_str()
- .expect("mismatch account id");
- let mismatch_public_identity = mismatch_account_json["public_identity"].clone();
-
- let myc = write_fake_myc(
- dir.path(),
- successful_status_script(
- sample_myc_status_payload(
- mismatch_account_id,
- &mismatch_public_identity,
- "conn_order_binding_02",
- )
- .to_string(),
- )
- .as_str(),
- );
-
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, auth_header| {
- recorded
- .lock()
- .expect("recorded requests lock")
- .push(MockRpcRequest {
- body,
- method: "unexpected".to_owned(),
- auth_header,
- });
- panic!("daemon write path should not be reached");
- });
- write_user_config(
- dir.path(),
- config_with_write_plane(
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{mismatch_account_id}"
-"#
- )
- .as_str(),
- server.url().as_str(),
- )
- .as_str(),
- );
-
- let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
- .args([
- "--json",
- "--signer",
- "myc",
- "--myc-executable",
- myc.to_str().expect("myc path"),
- "order",
- "submit",
- order_id,
- ])
- .output()
- .expect("run order submit");
-
- assert_eq!(submit_output.status.code(), Some(3));
- let submit_json: Value =
- serde_json::from_slice(submit_output.stdout.as_slice()).expect("submit json");
- assert_eq!(submit_json["state"], "unconfigured");
- assert_eq!(submit_json["signer_mode"], "myc");
- assert!(submit_json["reason"].as_str().is_some_and(|value| {
- value.contains("configured myc signer binding resolves user pubkey")
- }));
- assert!(requests.lock().expect("requests lock").is_empty());
-}
-
-#[test]
-fn order_submit_without_unique_matching_signer_session_exits_unconfigured() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let account_json: Value =
- serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
- let buyer_pubkey = account_json["public_identity"]["public_key_hex"]
- .as_str()
- .expect("buyer pubkey")
- .to_owned();
-
- let new_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- ])
- .output()
- .expect("run order new");
- assert!(new_output.status.success());
- let new_json: Value = serde_json::from_slice(new_output.stdout.as_slice()).expect("new json");
- let order_id = new_json["order_id"].as_str().expect("order id");
-
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded
- .lock()
- .expect("recorded requests lock")
- .push(MockRpcRequest {
- body: body.clone(),
- method: body["method"].as_str().unwrap_or_default().to_owned(),
- auth_header: None,
- });
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([
- sample_session(
- "sess_order_01",
- buyer_pubkey.as_str(),
- &["sign_event"],
- true
- ),
- sample_session(
- "sess_order_02",
- buyer_pubkey.as_str(),
- &["sign_event"],
- true
- )
- ])),
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
- .args(["--json", "order", "submit", order_id])
- .output()
- .expect("run order submit");
- assert_eq!(submit_output.status.code(), Some(3));
- let submit_json: Value =
- serde_json::from_slice(submit_output.stdout.as_slice()).expect("submit json");
- assert_eq!(submit_json["state"], "unconfigured");
- assert!(
- submit_json["reason"]
- .as_str()
- .expect("reason")
- .contains("multiple authorized signer sessions matched buyer pubkey")
- );
-
- let recorded_requests = requests.lock().expect("requests lock");
- assert_eq!(recorded_requests.len(), 1);
- assert_eq!(recorded_requests[0].method, "nip46.session.list");
-}
-
-#[test]
-fn order_submit_rejects_requested_session_that_mismatches_buyer_pubkey() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
-
- let account_output = order_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let new_output = order_command_in(dir.path())
- .args([
- "--json",
- "order",
- "new",
- "--listing",
- "pasture-eggs",
- "--listing-addr",
- "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg",
- "--bin",
- "bin-1",
- ])
- .output()
- .expect("run order new");
- assert!(new_output.status.success());
- let new_json: Value = serde_json::from_slice(new_output.stdout.as_slice()).expect("new json");
- let order_id = new_json["order_id"].as_str().expect("order id");
-
- let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
- let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |body, _auth_header| {
- recorded
- .lock()
- .expect("recorded requests lock")
- .push(MockRpcRequest {
- body: body.clone(),
- method: body["method"].as_str().unwrap_or_default().to_owned(),
- auth_header: None,
- });
- match body["method"].as_str().unwrap_or_default() {
- "nip46.session.list" => MockRpcResponse::success(json!([sample_session(
- "sess_wrong_01",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- &["sign_event"],
- true
- )])),
- other => panic!("unexpected mock rpc method {other}"),
- }
- });
- write_user_config(
- dir.path(),
- config_with_write_plane("", server.url().as_str()).as_str(),
- );
-
- let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
- .args([
- "--json",
- "order",
- "submit",
- order_id,
- "--signer-session-id",
- "sess_wrong_01",
- ])
- .output()
- .expect("run order submit");
- assert_eq!(submit_output.status.code(), Some(3));
- let submit_json: Value =
- serde_json::from_slice(submit_output.stdout.as_slice()).expect("submit json");
- assert_eq!(submit_json["state"], "unconfigured");
- assert!(
- submit_json["reason"]
- .as_str()
- .expect("reason")
- .contains("does not match buyer pubkey")
- );
-
- let recorded_requests = requests.lock().expect("requests lock");
- assert_eq!(recorded_requests.len(), 1);
- assert_eq!(recorded_requests[0].method, "nip46.session.list");
-}
-
-fn sample_session(
- session_id: &str,
- signer_pubkey: &str,
- permissions: &[&str],
- authorized: bool,
-) -> Value {
- sample_session_with_authority(
- session_id,
- signer_pubkey,
- permissions,
- authorized,
- None,
- None,
- )
-}
-
-fn sample_session_with_authority(
- session_id: &str,
- signer_pubkey: &str,
- permissions: &[&str],
- authorized: bool,
- account_identity_id: Option<&str>,
- provider_signer_session_id: Option<&str>,
-) -> Value {
- json!({
- "session_id": session_id,
- "role": "remote_signer",
- "client_pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
- "signer_pubkey": signer_pubkey,
- "user_pubkey": signer_pubkey,
- "relays": ["wss://relay.one"],
- "permissions": permissions,
- "auth_required": false,
- "authorized": authorized,
- "expires_in_secs": Value::Null,
- "signer_authority": account_identity_id.map(|account_identity_id| json!({
- "provider_runtime_id": "myc",
- "account_identity_id": account_identity_id,
- "provider_signer_session_id": provider_signer_session_id
- }))
- })
-}
-
-#[test]
-fn order_history_lists_submitted_order_drafts() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
- fs::create_dir_all(&drafts_dir).expect("create drafts dir");
- fs::write(
- drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml"),
- r#"version = 1
-kind = "order_draft_v1"
-listing_lookup = "fresh-eggs"
-buyer_account_id = "acct_demo"
-
-[order]
-order_id = "ord_AAAAAAAAAAAAAAAAAAAAAg"
-listing_addr = "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg"
-buyer_pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-seller_pubkey = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
-
-[[order.items]]
-bin_id = "bin-1"
-bin_count = 2
-
-[submission]
-job_id = "job_order_01"
-state = "accepted"
-command = "order.submit"
-submitted_at_unix = 1712720000
-"#,
- )
- .expect("write history draft");
-
- let json_output = order_command_in(dir.path())
- .args(["--json", "order", "history"])
- .output()
- .expect("run order history json");
- assert!(json_output.status.success());
- let json: Value = serde_json::from_slice(json_output.stdout.as_slice()).expect("history json");
- assert_eq!(json["count"], 1);
- assert_eq!(json["orders"][0]["id"], "ord_AAAAAAAAAAAAAAAAAAAAAg");
- assert_eq!(json["orders"][0]["state"], "accepted");
-
- let ndjson_output = order_command_in(dir.path())
- .args(["--ndjson", "order", "history"])
- .output()
- .expect("run order history ndjson");
- assert!(ndjson_output.status.success());
- let ndjson = String::from_utf8(ndjson_output.stdout).expect("history ndjson");
- let lines = ndjson.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 1);
- assert!(lines[0].contains("ord_AAAAAAAAAAAAAAAAAAAAAg"));
-}
-
-#[test]
-fn order_cancel_is_truthfully_narrowed_when_trade_chain_state_is_unavailable() {
- let _guard = order_test_guard();
- let dir = tempdir().expect("tempdir");
- let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
- fs::create_dir_all(&drafts_dir).expect("create drafts dir");
- fs::write(
- drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml"),
- r#"version = 1
-kind = "order_draft_v1"
-listing_lookup = "fresh-eggs"
-buyer_account_id = "acct_demo"
-
-[order]
-order_id = "ord_AAAAAAAAAAAAAAAAAAAAAg"
-listing_addr = "30402:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:AAAAAAAAAAAAAAAAAAAAAg"
-buyer_pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-seller_pubkey = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
-
-[[order.items]]
-bin_id = "bin-1"
-bin_count = 2
-
-[submission]
-job_id = "job_order_01"
-state = "accepted"
-command = "order.submit"
-"#,
- )
- .expect("write cancel draft");
-
- let output = order_command_in(dir.path())
- .args(["--json", "order", "cancel", "ord_AAAAAAAAAAAAAAAAAAAAAg"])
- .output()
- .expect("run order cancel");
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("cancel json");
- assert_eq!(json["state"], "unconfigured");
- assert!(
- json["reason"]
- .as_str()
- .expect("cancel reason")
- .contains("trade-chain")
- );
-}
diff --git a/tests/relay_net.rs b/tests/relay_net.rs
@@ -1,154 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn relay_ls_json_reports_workspace_configured_relays() {
- let dir = tempdir().expect("tempdir");
- let config_dir = dir.path().join("infra/local/runtime/radroots");
- fs::create_dir_all(&config_dir).expect("workspace config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.one\", \"wss://relay.two\"]\npublish_policy = \"any\"\n",
- )
- .expect("write workspace config");
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_CLI_PATHS_PROFILE", "repo_local")
- .env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", &config_dir)
- .args(["--json", "relay", "ls"])
- .output()
- .expect("run relay ls");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("relay json");
- assert_eq!(json["state"], "configured");
- assert_eq!(json["count"], 2);
- assert_eq!(json["publish_policy"], "any");
- assert_eq!(json["source"], "workspace config ยท local first");
- assert_eq!(json["relays"][0]["url"], "wss://relay.one");
- assert_eq!(json["relays"][0]["read"], true);
- assert_eq!(json["relays"][0]["write"], true);
- assert_eq!(json["relays"][1]["url"], "wss://relay.two");
-}
-
-#[test]
-fn relay_ls_ndjson_emits_one_object_per_relay() {
- let dir = tempdir().expect("tempdir");
- let output = cli_command_in(dir.path())
- .args([
- "--ndjson",
- "--relay",
- "wss://relay.one",
- "--relay",
- "wss://relay.two",
- "relay",
- "ls",
- ])
- .output()
- .expect("run relay ls");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines[0].contains("\"url\":\"wss://relay.one\""));
- assert!(lines[1].contains("\"url\":\"wss://relay.two\""));
-}
-
-#[test]
-fn relay_ls_without_relays_exits_unconfigured() {
- let dir = tempdir().expect("tempdir");
- let output = cli_command_in(dir.path())
- .args(["relay", "ls"])
- .output()
- .expect("run relay ls");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Not ready yet"));
- assert!(stdout.contains("Missing"));
- assert!(stdout.contains("Relay configuration"));
- assert!(stdout.contains("no relays are configured"));
-}
-
-#[test]
-fn net_status_json_reports_effective_network_configuration() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(init.status.success());
- let init_json: Value = serde_json::from_slice(init.stdout.as_slice()).expect("account json");
- let account_id = init_json["account"]["id"]
- .as_str()
- .expect("account id")
- .to_owned();
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "local",
- "--relay",
- "wss://relay.one",
- "--relay",
- "wss://relay.two",
- "net",
- "status",
- ])
- .output()
- .expect("run net status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("net json");
- assert_eq!(json["state"], "configured");
- assert_eq!(json["session"], "not_started");
- assert_eq!(json["relay_count"], 2);
- assert_eq!(json["publish_policy"], "any");
- assert_eq!(json["signer_mode"], "local");
- assert_eq!(json["account_resolution"]["source"], "default_account");
- assert_eq!(
- json["account_resolution"]["resolved_account"]["id"],
- account_id
- );
- assert_eq!(json["source"], "cli flags ยท local first");
-}
diff --git a/tests/runtime_management.rs b/tests/runtime_management.rs
@@ -1,367 +0,0 @@
-use std::fs;
-use std::fs::File;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-use std::thread;
-use std::time::Duration;
-
-use assert_cmd::prelude::*;
-use flate2::Compression;
-use flate2::write::GzEncoder;
-use serde_json::Value;
-use tar::Builder;
-use tempfile::tempdir;
-
-fn appdata_root(workdir: &Path) -> PathBuf {
- workdir.join("roaming").join("Radroots")
-}
-
-fn localappdata_root(workdir: &Path) -> PathBuf {
- workdir.join("local").join("Radroots")
-}
-
-fn interactive_root(workdir: &Path) -> PathBuf {
- if cfg!(windows) {
- localappdata_root(workdir)
- } else {
- workdir.join("home").join(".radroots")
- }
-}
-
-fn config_root(workdir: &Path) -> PathBuf {
- if cfg!(windows) {
- appdata_root(workdir).join("config")
- } else {
- interactive_root(workdir).join("config")
- }
-}
-
-fn cache_root(workdir: &Path) -> PathBuf {
- if cfg!(windows) {
- localappdata_root(workdir).join("cache")
- } else {
- interactive_root(workdir).join("cache")
- }
-}
-
-fn runtime_manager_registry_path(workdir: &Path) -> PathBuf {
- config_root(workdir).join("shared/runtime-manager/instances.toml")
-}
-
-fn runtime_manager_artifact_cache_dir(workdir: &Path) -> PathBuf {
- cache_root(workdir)
- .join("shared/runtime-manager/artifacts")
- .join("radrootsd")
- .join("stable")
-}
-
-fn runtime_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[cfg(not(windows))]
-fn current_server_target_id() -> &'static str {
- if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
- "aarch64-apple-darwin"
- } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
- "x86_64-apple-darwin"
- } else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
- "aarch64-unknown-linux-gnu"
- } else if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
- "x86_64-unknown-linux-gnu"
- } else {
- panic!("unsupported host target for runtime-management tests")
- }
-}
-
-#[cfg(not(windows))]
-fn write_cached_radrootsd_artifact(workdir: &Path) -> PathBuf {
- let artifact_dir = runtime_manager_artifact_cache_dir(workdir);
- fs::create_dir_all(&artifact_dir).expect("artifact dir");
- let file_name = format!(
- "radrootsd-0.1.0-alpha.2-{}.tar.gz",
- current_server_target_id()
- );
- let archive_path = artifact_dir.join(file_name);
- let script = artifact_dir.join("radrootsd");
- fs::write(
- &script,
- "#!/bin/sh\nprintf 'managed radrootsd started\\n' >> \"${TMPDIR:-/tmp}/radrootsd-managed.log\"\nsleep 30\n",
- )
- .expect("write script");
- let file = File::create(&archive_path).expect("archive file");
- let encoder = GzEncoder::new(file, Compression::default());
- let mut builder = Builder::new(encoder);
- builder
- .append_path_with_name(&script, "radrootsd/bin/radrootsd")
- .expect("append script");
- builder.finish().expect("finish archive");
- archive_path
-}
-
-#[test]
-fn runtime_status_reports_active_managed_target_truth() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_command_in(dir.path())
- .args(["--json", "runtime", "status", "radrootsd"])
- .output()
- .expect("runtime status");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["runtime_id"], "radrootsd");
- assert_eq!(json["runtime_group"], "active_managed_target");
- assert_eq!(json["management_posture"], "active_managed_target");
- assert_eq!(json["state"], "not_installed");
- assert_eq!(json["install_state"], "not_installed");
- assert_eq!(json["health_state"], "not_installed");
- assert_eq!(json["instance_id"], "local");
- assert_eq!(json["instance_source"], "bootstrap_default");
- assert_eq!(json["management_mode"], "interactive_user_managed");
-}
-
-#[test]
-fn runtime_status_reports_defined_future_target_truth() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_command_in(dir.path())
- .args(["--json", "runtime", "status", "myc"])
- .output()
- .expect("runtime status");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["runtime_id"], "myc");
- assert_eq!(json["runtime_group"], "defined_managed_target");
- assert_eq!(json["management_posture"], "defined_future_target");
- assert_eq!(json["state"], "defined_not_active");
- assert_eq!(json["lifecycle_actions"], Value::Array(vec![]));
-}
-
-#[cfg(not(windows))]
-#[test]
-fn runtime_manages_radrootsd_lifecycle_end_to_end() {
- let dir = tempdir().expect("tempdir");
- let artifact_path = write_cached_radrootsd_artifact(dir.path());
-
- let install = runtime_command_in(dir.path())
- .args(["--json", "runtime", "install", "radrootsd"])
- .output()
- .expect("runtime install");
- assert!(install.status.success());
- let install_json: Value =
- serde_json::from_slice(install.stdout.as_slice()).expect("install json");
- assert_eq!(install_json["action"], "install");
- assert_eq!(install_json["state"], "configured");
- assert!(
- install_json["detail"]
- .as_str()
- .expect("detail")
- .contains(artifact_path.display().to_string().as_str())
- );
-
- let registry_path = runtime_manager_registry_path(dir.path());
- let registry_raw = fs::read_to_string(®istry_path).expect("registry");
- assert!(registry_raw.contains("health_endpoint = \"http://127.0.0.1:7070\""));
- assert!(registry_raw.contains("secret_material_ref = "));
-
- let start = runtime_command_in(dir.path())
- .args(["--json", "runtime", "start", "radrootsd"])
- .output()
- .expect("runtime start");
- assert!(start.status.success());
- let start_json: Value = serde_json::from_slice(start.stdout.as_slice()).expect("start json");
- assert_eq!(start_json["state"], "running");
-
- thread::sleep(Duration::from_millis(150));
-
- let running_status = runtime_command_in(dir.path())
- .args(["--json", "runtime", "status", "radrootsd"])
- .output()
- .expect("runtime status");
- assert!(running_status.status.success());
- let running_json: Value =
- serde_json::from_slice(running_status.stdout.as_slice()).expect("status json");
- assert_eq!(running_json["state"], "configured");
- assert_eq!(running_json["health_state"], "running");
-
- let config_set = runtime_command_in(dir.path())
- .args([
- "--json",
- "runtime",
- "config",
- "set",
- "radrootsd",
- "config.rpc.addr",
- "127.0.0.1:7444",
- ])
- .output()
- .expect("runtime config set");
- assert!(config_set.status.success());
- let config_set_json: Value =
- serde_json::from_slice(config_set.stdout.as_slice()).expect("config set json");
- assert_eq!(config_set_json["state"], "configured");
-
- let updated_registry_raw = fs::read_to_string(®istry_path).expect("updated registry");
- assert!(updated_registry_raw.contains("health_endpoint = \"http://127.0.0.1:7444\""));
-
- let stop = runtime_command_in(dir.path())
- .args(["--json", "runtime", "stop", "radrootsd"])
- .output()
- .expect("runtime stop");
- assert!(stop.status.success());
- let stop_json: Value = serde_json::from_slice(stop.stdout.as_slice()).expect("stop json");
- assert_eq!(stop_json["state"], "stopped");
-
- let uninstall = runtime_command_in(dir.path())
- .args(["--json", "runtime", "uninstall", "radrootsd"])
- .output()
- .expect("runtime uninstall");
- assert!(uninstall.status.success());
- let uninstall_json: Value =
- serde_json::from_slice(uninstall.stdout.as_slice()).expect("uninstall json");
- assert_eq!(uninstall_json["state"], "uninstalled");
-
- let final_status = runtime_command_in(dir.path())
- .args(["--json", "runtime", "status", "radrootsd"])
- .output()
- .expect("runtime status after uninstall");
- assert!(final_status.status.success());
- let final_json: Value =
- serde_json::from_slice(final_status.stdout.as_slice()).expect("final status json");
- assert_eq!(final_json["state"], "not_installed");
- assert_eq!(final_json["health_state"], "not_installed");
-}
-
-#[test]
-fn runtime_logs_reports_managed_log_locations() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_command_in(dir.path())
- .args(["--json", "runtime", "logs", "radrootsd"])
- .output()
- .expect("runtime logs");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["runtime_id"], "radrootsd");
- assert_eq!(json["state"], "ready");
- assert!(
- json["stdout_log_path"]
- .as_str()
- .expect("stdout log path")
- .ends_with("shared/runtime-manager/radrootsd/local/stdout.log")
- );
- assert!(
- json["stderr_log_path"]
- .as_str()
- .expect("stderr log path")
- .ends_with("shared/runtime-manager/radrootsd/local/stderr.log")
- );
-}
-
-#[test]
-fn runtime_config_show_uses_registered_instance_config_path() {
- let dir = tempdir().expect("tempdir");
- let registry_path = runtime_manager_registry_path(dir.path());
- let config_path = dir.path().join("managed").join("radrootsd-local.toml");
- let token_path = dir.path().join("managed").join("bridge-token.txt");
- fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir");
- fs::write(
- &config_path,
- "[metadata]\nname = \"managed-radrootsd\"\n[config.rpc]\naddr = \"127.0.0.1:7070\"\n[config.bridge]\nenabled = true\nbearer_token = \"redacted\"\n",
- )
- .expect("write config");
- fs::write(&token_path, "redacted").expect("write token");
- fs::create_dir_all(registry_path.parent().expect("registry parent")).expect("registry dir");
- fs::write(
- ®istry_path,
- format!(
- r#"schema = "radroots_runtime-instance-registry"
-schema_version = 1
-
-[[instances]]
-runtime_id = "radrootsd"
-instance_id = "local"
-management_mode = "interactive_user_managed"
-install_state = "configured"
-binary_path = "{binary_path}"
-config_path = "{config_path}"
-logs_path = "{logs_path}"
-run_path = "{run_path}"
-installed_version = "0.1.0"
-health_endpoint = "http://127.0.0.1:7070"
-secret_material_ref = "{secret_material_ref}"
-"#,
- binary_path = dir.path().join("bin/radrootsd").display(),
- config_path = config_path.display(),
- logs_path = dir.path().join("managed/logs/radrootsd-local").display(),
- run_path = dir.path().join("managed/run/radrootsd-local").display(),
- secret_material_ref = token_path.display(),
- ),
- )
- .expect("write registry");
-
- let output = runtime_command_in(dir.path())
- .args(["--json", "runtime", "config", "show", "radrootsd"])
- .output()
- .expect("runtime config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["runtime_id"], "radrootsd");
- assert_eq!(json["config_format"], "toml");
- assert_eq!(json["config_path"], config_path.display().to_string());
- assert_eq!(json["config_present"], true);
- assert_eq!(json["requires_bootstrap_secret"], true);
- assert_eq!(json["requires_config_bootstrap"], true);
-}
-
-#[test]
-fn runtime_logs_rejects_bootstrap_only_runtime() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_command_in(dir.path())
- .args(["--json", "runtime", "logs", "hyf"])
- .output()
- .expect("runtime logs");
-
- assert_eq!(output.status.code(), Some(5));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["runtime_id"], "hyf");
- assert_eq!(json["runtime_group"], "bootstrap_only");
- assert_eq!(json["state"], "unsupported");
-}
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -1,873 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn appdata_root(workdir: &Path) -> std::path::PathBuf {
- workdir.join("roaming").join("Radroots")
-}
-
-fn localappdata_root(workdir: &Path) -> std::path::PathBuf {
- workdir.join("local").join("Radroots")
-}
-
-fn interactive_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- localappdata_root(workdir)
- } else {
- workdir.join("home").join(".radroots")
- }
-}
-
-fn config_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- appdata_root(workdir).join("config")
- } else {
- interactive_root(workdir).join("config")
- }
-}
-
-fn runtime_manager_registry_path(workdir: &Path) -> std::path::PathBuf {
- config_root(workdir).join("shared/runtime-manager/instances.toml")
-}
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- localappdata_root(workdir).join("data")
- } else {
- interactive_root(workdir).join("data")
- }
-}
-
-fn logs_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- localappdata_root(workdir).join("logs")
- } else {
- interactive_root(workdir).join("logs")
- }
-}
-
-fn secrets_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- appdata_root(workdir).join("secrets")
- } else {
- interactive_root(workdir).join("secrets")
- }
-}
-
-fn runtime_show_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn binding_by_capability<'a>(json: &'a Value, capability_id: &str) -> &'a Value {
- json["capability_bindings"]
- .as_array()
- .expect("capability bindings array")
- .iter()
- .find(|binding| binding["capability_id"] == capability_id)
- .expect("binding present")
-}
-
-#[test]
-fn config_show_json_reports_default_bootstrap_state() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_show_command_in(dir.path())
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["source"], "local runtime state");
- assert_eq!(json["output"]["format"], "json");
- assert_eq!(json["output"]["verbosity"], "normal");
- assert_eq!(json["output"]["color"], true);
- assert_eq!(json["output"]["dry_run"], false);
- assert_eq!(json["interaction"]["input_enabled"], true);
- assert_eq!(json["interaction"]["assume_yes"], false);
- assert_eq!(json["paths"]["profile"], "interactive_user");
- assert_eq!(json["paths"]["profile_source"], "default");
- assert_eq!(json["paths"]["root_source"], "host_defaults");
- assert_eq!(json["paths"]["repo_local_root"], Value::Null);
- assert_eq!(json["paths"]["repo_local_root_source"], Value::Null);
- assert_eq!(
- json["paths"]["subordinate_path_override_source"],
- "runtime_config"
- );
- assert_eq!(json["paths"]["allowed_profiles"][0], "interactive_user");
- assert_eq!(json["paths"]["allowed_profiles"][1], "repo_local");
- assert_eq!(json["paths"]["app_namespace"], "apps/cli");
- assert_eq!(
- json["paths"]["shared_accounts_namespace"],
- "shared/accounts"
- );
- assert_eq!(
- json["paths"]["shared_identities_namespace"],
- "shared/identities"
- );
- assert_eq!(
- json["paths"]["app_config_path"],
- config_root(dir.path())
- .join("apps/cli/config.toml")
- .display()
- .to_string()
- );
- assert_eq!(json["paths"]["workspace_config_enabled"], false);
- assert_eq!(json["paths"]["workspace_config_path"], Value::Null);
- assert_eq!(
- json["paths"]["app_data_root"],
- data_root(dir.path()).join("apps/cli").display().to_string()
- );
- assert_eq!(
- json["paths"]["app_logs_root"],
- logs_root(dir.path()).join("apps/cli").display().to_string()
- );
- assert_eq!(
- json["paths"]["shared_accounts_data_root"],
- data_root(dir.path())
- .join("shared/accounts")
- .display()
- .to_string()
- );
- assert_eq!(
- json["paths"]["shared_accounts_secrets_root"],
- secrets_root(dir.path())
- .join("shared/accounts")
- .display()
- .to_string()
- );
- assert_eq!(
- json["paths"]["default_identity_path"],
- secrets_root(dir.path())
- .join("shared/identities/default.json")
- .display()
- .to_string()
- );
- assert_eq!(
- json["migration"]["posture"],
- "explicit_operator_import_required"
- );
- assert_eq!(json["migration"]["state"], "ready");
- assert_eq!(json["migration"]["silent_startup_relocation"], false);
- assert_eq!(
- json["migration"]["compatibility_window"],
- "detect_and_report_only"
- );
- assert_eq!(
- json["migration"]["detected_legacy_paths"],
- Value::Array(vec![])
- );
- assert_eq!(json["migration"]["actions"], Value::Array(vec![]));
- assert_eq!(json["logging"]["initialized"], true);
- assert_eq!(json["logging"]["stdout"], false);
- assert_eq!(
- json["logging"]["directory"],
- logs_root(dir.path()).join("apps/cli").display().to_string()
- );
- assert_eq!(json["config_files"]["user_present"], false);
- assert_eq!(json["config_files"]["workspace_present"], false);
- assert_eq!(json["account"]["selector"], Value::Null);
- assert_eq!(
- json["account"]["store_path"],
- data_root(dir.path())
- .join("shared/accounts/store.json")
- .display()
- .to_string()
- );
- assert_eq!(
- json["account"]["secrets_dir"],
- secrets_root(dir.path())
- .join("shared/accounts")
- .display()
- .to_string()
- );
- assert_eq!(
- json["account"]["identity_path"],
- secrets_root(dir.path())
- .join("shared/identities/default.json")
- .display()
- .to_string()
- );
- assert_eq!(
- json["account"]["secret_backend"]["contract_default_backend"],
- "host_vault"
- );
- assert_eq!(
- json["account"]["secret_backend"]["contract_default_fallback"],
- "encrypted_file"
- );
- assert_eq!(
- json["account"]["secret_backend"]["allowed_backends"]
- .as_array()
- .expect("allowed backends")
- .len(),
- 2
- );
- assert_eq!(
- json["account"]["secret_backend"]["allowed_backends"][0],
- "host_vault"
- );
- assert_eq!(
- json["account"]["secret_backend"]["allowed_backends"][1],
- "encrypted_file"
- );
- assert_eq!(
- json["account"]["secret_backend"]["host_vault_policy"],
- "desktop"
- );
- assert_eq!(
- json["account"]["secret_backend"]["uses_protected_store"],
- true
- );
- assert_eq!(
- json["account"]["secret_backend"]["configured_primary"],
- "host_vault"
- );
- assert_eq!(
- json["account"]["secret_backend"]["configured_fallback"],
- "encrypted_file"
- );
- assert_eq!(json["account"]["secret_backend"]["state"], "ready");
- assert_eq!(
- json["account"]["secret_backend"]["active_backend"],
- "encrypted_file"
- );
- assert_eq!(json["account"]["secret_backend"]["used_fallback"], true);
- assert_eq!(json["signer"]["mode"], "local");
- assert_eq!(json["relay"]["count"], 0);
- assert_eq!(json["relay"]["publish_policy"], "any");
- assert_eq!(json["relay"]["source"], "defaults ยท local first");
- assert_eq!(
- json["local"]["root"],
- data_root(dir.path())
- .join("apps/cli/replica")
- .display()
- .to_string()
- );
- assert_eq!(
- json["local"]["replica_db_path"],
- data_root(dir.path())
- .join("apps/cli/replica/replica.sqlite")
- .display()
- .to_string()
- );
- assert_eq!(json["myc"]["executable"], "myc");
- assert_eq!(json["myc"]["status_timeout_ms"], 2000);
- assert_eq!(json["write_plane"]["provider_runtime_id"], "radrootsd");
- assert_eq!(
- json["write_plane"]["binding_model"],
- "daemon_backed_jsonrpc"
- );
- assert_eq!(json["write_plane"]["state"], "unconfigured");
- assert_eq!(json["write_plane"]["provenance"], "unavailable");
- assert_eq!(
- json["write_plane"]["source"],
- "no explicit capability binding or managed preferred default"
- );
- assert!(json["write_plane"]["target"].is_null());
- assert_eq!(json["write_plane"]["bridge_auth_configured"], false);
- assert_eq!(json["workflow"]["provider_runtime_id"], "rhi");
- assert_eq!(json["workflow"]["binding_model"], "out_of_process_worker");
- assert_eq!(json["workflow"]["state"], "not_configured");
- assert_eq!(json["workflow"]["provenance"], "unavailable");
- assert_eq!(json["workflow"]["source"], "no explicit capability binding");
- assert_eq!(json["workflow"]["hyf_helper_state"], "not_implied");
- assert_eq!(json["hyf_provider"]["provider_runtime_id"], "hyf");
- assert_eq!(json["hyf_provider"]["binding_model"], "stdio_service");
- assert_eq!(json["hyf_provider"]["state"], "disabled");
- assert_eq!(json["hyf_provider"]["provenance"], "disabled");
- assert_eq!(
- json["hyf_provider"]["source"],
- "hyf status control request ยท local first"
- );
- assert_eq!(json["rpc"]["url"], "http://127.0.0.1:7070");
- assert_eq!(json["rpc"]["bridge_auth_configured"], false);
- assert_eq!(
- json["resolved_providers"]
- .as_array()
- .expect("resolved providers")
- .len(),
- 3
- );
- assert_eq!(
- json["capability_bindings"]
- .as_array()
- .expect("capability bindings")
- .len(),
- 4
- );
- let signer = binding_by_capability(&json, "signer.remote_nip46");
- assert_eq!(signer["provider_runtime_id"], "myc");
- assert_eq!(signer["binding_model"], "session_authorized_remote_signer");
- assert_eq!(signer["state"], "disabled");
- assert_eq!(signer["source"], "independent local signer mode");
- let write = binding_by_capability(&json, "write_plane.trade_jsonrpc");
- assert_eq!(write["provider_runtime_id"], "radrootsd");
- assert_eq!(write["binding_model"], "daemon_backed_jsonrpc");
- assert_eq!(write["state"], "not_configured");
- let workflow = binding_by_capability(&json, "workflow.trade");
- assert_eq!(workflow["provider_runtime_id"], "rhi");
- assert_eq!(workflow["binding_model"], "out_of_process_worker");
- assert_eq!(workflow["state"], "not_configured");
- let inference = binding_by_capability(&json, "inference.hyf_stdio");
- assert_eq!(inference["provider_runtime_id"], "hyf");
- assert_eq!(inference["binding_model"], "stdio_service");
- assert_eq!(inference["state"], "disabled");
- assert_eq!(inference["source"], "hyf disabled by config");
-}
-
-#[test]
-fn config_show_machine_output_rejects_stdout_logging() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_show_command_in(dir.path())
- .args(["--json", "--log-stdout", "config", "show"])
- .output()
- .expect("run config show");
-
- 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("stdout logging"));
- assert!(stderr.contains("json output"));
-
- let env_output = runtime_show_command_in(dir.path())
- .env("RADROOTS_CLI_LOGGING_STDOUT", "true")
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert_eq!(env_output.status.code(), Some(2));
- assert!(env_output.stdout.is_empty());
- let stderr = String::from_utf8(env_output.stderr).expect("utf8 stderr");
- assert!(stderr.contains("RADROOTS_CLI_LOGGING_STDOUT"));
-}
-
-#[test]
-fn config_show_json_reports_detected_legacy_cli_paths_without_moving_them() {
- let dir = tempdir().expect("tempdir");
- let home = dir.path().join("home");
- let old_config = home.join(".config/radroots/config.toml");
- let old_state_root = home.join(".local/share/radroots");
- fs::create_dir_all(old_config.parent().expect("old config parent")).expect("old config dir");
- fs::create_dir_all(old_state_root.join("accounts")).expect("old state dir");
- fs::write(&old_config, "[relay]\nurls = []\n").expect("old config");
- fs::write(old_state_root.join("accounts/store.json"), "{}").expect("old store");
-
- let output = runtime_show_command_in(dir.path())
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
-
- assert_eq!(json["migration"]["state"], "legacy_state_detected");
- assert_eq!(json["migration"]["silent_startup_relocation"], false);
- let detected = json["migration"]["detected_legacy_paths"]
- .as_array()
- .expect("detected legacy paths array");
- assert_eq!(detected.len(), 2);
- assert_eq!(detected[0]["id"], "cli_user_config_v0");
- assert_eq!(detected[0]["path"], old_config.display().to_string());
- assert_eq!(
- detected[0]["destination"],
- config_root(dir.path())
- .join("apps/cli/config.toml")
- .display()
- .to_string()
- );
- assert_eq!(detected[1]["id"], "cli_user_state_root_v0");
- assert_eq!(detected[1]["path"], old_state_root.display().to_string());
- assert!(
- json["migration"]["actions"]
- .as_array()
- .expect("actions")
- .iter()
- .any(|action| action
- .as_str()
- .is_some_and(|value| value.contains("startup did not move legacy data")))
- );
-}
-
-#[test]
-fn config_show_json_reports_repo_local_paths_when_requested() {
- let dir = tempdir().expect("tempdir");
- let repo_local_root = dir.path().join(".local/radroots/dev");
- let output = runtime_show_command_in(dir.path())
- .env("RADROOTS_CLI_PATHS_PROFILE", "repo_local")
- .env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", &repo_local_root)
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
-
- assert_eq!(json["paths"]["profile"], "repo_local");
- assert_eq!(
- json["paths"]["profile_source"],
- "process_env:RADROOTS_CLI_PATHS_PROFILE"
- );
- assert_eq!(json["paths"]["root_source"], "repo_local_root");
- assert_eq!(
- json["paths"]["repo_local_root"],
- repo_local_root.display().to_string()
- );
- assert_eq!(
- json["paths"]["repo_local_root_source"],
- "process_env:RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT"
- );
- assert_eq!(json["paths"]["allowed_profiles"][0], "interactive_user");
- assert_eq!(json["paths"]["allowed_profiles"][1], "repo_local");
- assert_eq!(
- json["paths"]["app_config_path"],
- repo_local_root
- .join("config/apps/cli/config.toml")
- .display()
- .to_string()
- );
- assert_eq!(
- json["paths"]["workspace_config_path"],
- repo_local_root.join("config.toml").display().to_string()
- );
- assert_eq!(json["paths"]["workspace_config_enabled"], true);
- assert_eq!(
- json["paths"]["app_data_root"],
- repo_local_root.join("data/apps/cli").display().to_string()
- );
- assert_eq!(
- json["paths"]["app_logs_root"],
- repo_local_root.join("logs/apps/cli").display().to_string()
- );
- assert_eq!(
- json["paths"]["shared_accounts_data_root"],
- repo_local_root
- .join("data/shared/accounts")
- .display()
- .to_string()
- );
- assert_eq!(
- json["paths"]["shared_accounts_secrets_root"],
- repo_local_root
- .join("secrets/shared/accounts")
- .display()
- .to_string()
- );
- assert_eq!(
- json["paths"]["default_identity_path"],
- repo_local_root
- .join("secrets/shared/identities/default.json")
- .display()
- .to_string()
- );
-}
-
-#[test]
-fn config_show_json_reads_repo_local_workspace_config_from_explicit_root() {
- let dir = tempdir().expect("tempdir");
- let repo_local_root = dir.path().join("repo-runtime");
- fs::create_dir_all(&repo_local_root).expect("repo-local root");
- fs::write(
- repo_local_root.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.repo-local\"]\npublish_policy = \"any\"\n",
- )
- .expect("write repo-local workspace config");
-
- let output = runtime_show_command_in(dir.path())
- .env("RADROOTS_CLI_PATHS_PROFILE", "repo_local")
- .env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", &repo_local_root)
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(
- json["paths"]["workspace_config_path"],
- repo_local_root.join("config.toml").display().to_string()
- );
- assert_eq!(json["paths"]["workspace_config_enabled"], true);
- assert_eq!(json["config_files"]["workspace_present"], true);
- assert_eq!(json["relay"]["count"], 1);
- assert_eq!(json["relay"]["urls"][0], "wss://relay.repo-local");
- assert_eq!(json["relay"]["source"], "workspace config ยท local first");
-}
-
-#[test]
-fn config_show_json_reflects_environment_configuration() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_show_command_in(dir.path())
- .env("RADROOTS_OUTPUT", "json")
- .env("RADROOTS_LOG_FILTER", "debug")
- .env("RADROOTS_LOG_DIR", "logs/runtime")
- .env("RADROOTS_LOG_STDOUT", "false")
- .env("RADROOTS_ACCOUNT", "acct_demo")
- .env("RADROOTS_IDENTITY_PATH", "state/identity.json")
- .env("RADROOTS_SIGNER", "myc")
- .env("RADROOTS_RELAYS", "wss://relay.one,wss://relay.two")
- .env("RADROOTS_MYC_EXECUTABLE", "bin/myc")
- .env("RADROOTS_MYC_STATUS_TIMEOUT_MS", "3500")
- .env("RADROOTS_RPC_URL", "https://rpc.radroots.test/jsonrpc")
- .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
- .args(["config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["output"]["format"], "json");
- assert_eq!(json["logging"]["filter"], "debug");
- assert_eq!(json["logging"]["directory"], "logs/runtime");
- assert_eq!(json["account"]["selector"], "acct_demo");
- assert_eq!(json["account"]["identity_path"], "state/identity.json");
- assert_eq!(
- json["account"]["secret_backend"]["active_backend"],
- "encrypted_file"
- );
- assert_eq!(json["signer"]["mode"], "myc");
- assert_eq!(json["relay"]["count"], 2);
- assert_eq!(json["relay"]["urls"][0], "wss://relay.one");
- assert_eq!(json["relay"]["source"], "environment ยท local first");
- assert_eq!(json["myc"]["executable"], "bin/myc");
- assert_eq!(json["myc"]["status_timeout_ms"], 3500);
- assert_eq!(json["rpc"]["url"], "https://rpc.radroots.test/jsonrpc");
- assert_eq!(json["rpc"]["bridge_auth_configured"], true);
-}
-
-#[test]
-fn config_show_json_reflects_global_output_flags() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_show_command_in(dir.path())
- .args([
- "--output",
- "json",
- "--trace",
- "--dry-run",
- "--no-color",
- "--no-input",
- "--yes",
- "config",
- "show",
- ])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["output"]["format"], "json");
- assert_eq!(json["output"]["verbosity"], "trace");
- assert_eq!(json["output"]["color"], false);
- assert_eq!(json["output"]["dry_run"], true);
- assert_eq!(json["interaction"]["input_enabled"], false);
- assert_eq!(json["interaction"]["assume_yes"], true);
-}
-
-#[test]
-fn config_show_json_reads_logging_from_default_env_file() {
- let temp = tempdir().expect("tempdir");
- let env_path = temp.path().join(".env");
- let logs_dir = temp.path().join("logs").join("radroots-cli");
- fs::write(
- &env_path,
- format!(
- "RADROOTS_CLI_LOGGING_FILTER=debug,radroots_cli=trace\nRADROOTS_CLI_LOGGING_OUTPUT_DIR={}\nRADROOTS_CLI_LOGGING_STDOUT=false\n",
- logs_dir.display()
- ),
- )
- .expect("write env file");
-
- let output = runtime_show_command_in(temp.path())
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["logging"]["filter"], "debug,radroots_cli=trace");
- assert_eq!(json["logging"]["directory"], logs_dir.display().to_string());
- let current_file = json["logging"]["current_file"]
- .as_str()
- .expect("current log file");
- assert!(current_file.starts_with(logs_dir.display().to_string().as_str()));
- assert!(std::path::Path::new(current_file).exists());
-}
-
-#[test]
-fn config_show_json_reads_workspace_relay_config() {
- let dir = tempdir().expect("tempdir");
- let config_dir = dir.path().join("infra/local/runtime/radroots");
- fs::create_dir_all(&config_dir).expect("workspace config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.workspace\", \"wss://relay.backup\"]\npublish_policy = \"any\"\n",
- )
- .expect("write workspace config");
-
- let output = runtime_show_command_in(dir.path())
- .env("RADROOTS_CLI_PATHS_PROFILE", "repo_local")
- .env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", &config_dir)
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["config_files"]["workspace_present"], true);
- assert_eq!(json["relay"]["count"], 2);
- assert_eq!(json["relay"]["urls"][0], "wss://relay.workspace");
- assert_eq!(json["relay"]["urls"][1], "wss://relay.backup");
- assert_eq!(json["relay"]["source"], "workspace config ยท local first");
-}
-
-#[test]
-fn config_show_reads_workspace_rpc_config() {
- let dir = tempdir().expect("tempdir");
- let config_dir = dir.path().join("infra/local/runtime/radroots");
- fs::create_dir_all(&config_dir).expect("workspace config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[rpc]\nurl = \"https://rpc.workspace.test/jsonrpc\"\n",
- )
- .expect("write workspace config");
-
- let output = runtime_show_command_in(dir.path())
- .env("RADROOTS_CLI_PATHS_PROFILE", "repo_local")
- .env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", &config_dir)
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["rpc"]["url"], "https://rpc.workspace.test/jsonrpc");
- assert_eq!(json["rpc"]["bridge_auth_configured"], false);
-}
-
-#[test]
-fn config_show_reports_explicit_capability_bindings() {
- let dir = tempdir().expect("tempdir");
- let workspace_config_dir = dir.path().join("infra/local/runtime/radroots");
- let user_config_dir = workspace_config_dir.join("config/apps/cli");
- fs::create_dir_all(&workspace_config_dir).expect("workspace config dir");
- fs::create_dir_all(&user_config_dir).expect("user config dir");
- fs::write(
- workspace_config_dir.join("config.toml"),
- r#"
-[[capability_binding]]
-capability = "write_plane.trade_jsonrpc"
-provider = "radrootsd"
-target_kind = "explicit_endpoint"
-target = "https://rpc.workspace.test/jsonrpc"
-
-[[capability_binding]]
-capability = "inference.hyf_stdio"
-provider = "hyf"
-target_kind = "managed_instance"
-target = "workspace-hyf"
-"#,
- )
- .expect("write workspace config");
- fs::write(
- user_config_dir.join("config.toml"),
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "acct_demo"
-signer_session_ref = "session_demo"
-
-[[capability_binding]]
-capability = "workflow.trade"
-provider = "rhi"
-target_kind = "managed_instance"
-target = "workflow-default"
-
-[[capability_binding]]
-capability = "inference.hyf_stdio"
-provider = "hyf"
-target_kind = "explicit_endpoint"
-target = "bin/hyfd-user"
-"#,
- )
- .expect("write user config");
-
- let output = runtime_show_command_in(dir.path())
- .env("RADROOTS_CLI_PATHS_PROFILE", "repo_local")
- .env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", &workspace_config_dir)
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
-
- let signer = binding_by_capability(&json, "signer.remote_nip46");
- assert_eq!(signer["state"], "configured");
- assert_eq!(signer["source"], "user config [[capability_binding]]");
- assert_eq!(signer["target_kind"], "managed_instance");
- assert_eq!(signer["target"], "default");
- assert_eq!(signer["managed_account_ref"], "acct_demo");
- assert_eq!(signer["signer_session_ref"], "session_demo");
-
- let write = binding_by_capability(&json, "write_plane.trade_jsonrpc");
- assert_eq!(write["state"], "configured");
- assert_eq!(write["source"], "workspace config [[capability_binding]]");
- assert_eq!(write["target_kind"], "explicit_endpoint");
- assert_eq!(write["target"], "https://rpc.workspace.test/jsonrpc");
-
- let workflow = binding_by_capability(&json, "workflow.trade");
- assert_eq!(workflow["state"], "configured");
- assert_eq!(workflow["source"], "user config [[capability_binding]]");
- assert_eq!(workflow["target_kind"], "managed_instance");
- assert_eq!(workflow["target"], "workflow-default");
- assert_eq!(json["workflow"]["provider_runtime_id"], "rhi");
- assert_eq!(json["workflow"]["state"], "unavailable");
- assert_eq!(json["workflow"]["provenance"], "managed_default");
- assert_eq!(
- json["workflow"]["source"],
- "user config [[capability_binding]]"
- );
- assert_eq!(json["workflow"]["target_kind"], "managed_instance");
- assert_eq!(json["workflow"]["target"], "workflow-default");
- assert_eq!(json["workflow"]["hyf_helper_state"], "not_implied");
- assert!(
- json["workflow"]["hyf_helper_detail"]
- .as_str()
- .is_some_and(|detail| detail.contains("do not imply"))
- );
- assert_eq!(json["write_plane"]["state"], "unconfigured");
- assert_eq!(json["write_plane"]["provenance"], "explicit_binding");
- assert_eq!(
- json["write_plane"]["source"],
- "workspace config [[capability_binding]]"
- );
- assert_eq!(json["write_plane"]["target_kind"], "explicit_endpoint");
- assert_eq!(
- json["write_plane"]["target"],
- "https://rpc.workspace.test/jsonrpc"
- );
- assert_eq!(json["write_plane"]["bridge_auth_configured"], false);
- assert_eq!(json["hyf_provider"]["provider_runtime_id"], "hyf");
- assert_eq!(json["hyf_provider"]["provenance"], "explicit_binding");
- assert_eq!(json["hyf_provider"]["target_kind"], "explicit_endpoint");
- assert_eq!(json["hyf_provider"]["target"], "bin/hyfd-user");
- assert_eq!(json["hyf_provider"]["executable"], "bin/hyfd-user");
-
- let inference = binding_by_capability(&json, "inference.hyf_stdio");
- assert_eq!(inference["state"], "configured");
- assert_eq!(inference["source"], "user config [[capability_binding]]");
- assert_eq!(inference["target_kind"], "explicit_endpoint");
- assert_eq!(inference["target"], "bin/hyfd-user");
-}
-
-#[test]
-fn config_show_uses_managed_default_write_plane_when_local_instance_exists() {
- let dir = tempdir().expect("tempdir");
- let registry_path = runtime_manager_registry_path(dir.path());
- fs::create_dir_all(registry_path.parent().expect("registry parent")).expect("registry dir");
- let managed_config_path = dir.path().join("managed-radrootsd.toml");
- let bridge_token_path = dir.path().join("managed-bridge-token.txt");
- fs::write(
- &managed_config_path,
- "[metadata]\nname = \"managed-radrootsd\"\n",
- )
- .expect("write managed config");
- fs::write(&bridge_token_path, "managed-bridge-token").expect("write managed token");
- fs::write(
- ®istry_path,
- format!(
- r#"schema = "radroots_runtime-instance-registry"
-schema_version = 1
-
-[[instances]]
-runtime_id = "radrootsd"
-instance_id = "local"
-management_mode = "interactive_user_managed"
-install_state = "configured"
-binary_path = "/tmp/radrootsd"
-config_path = "{}"
-logs_path = "/tmp/radrootsd/logs"
-run_path = "/tmp/radrootsd/run"
-installed_version = "0.1.0"
-health_endpoint = "http://127.0.0.1:7444"
-secret_material_ref = "{}"
-"#,
- managed_config_path.display(),
- bridge_token_path.display()
- ),
- )
- .expect("write managed registry");
-
- let output = runtime_show_command_in(dir.path())
- .args(["--json", "config", "show"])
- .output()
- .expect("run config show");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
- assert_eq!(json["write_plane"]["state"], "configured");
- assert_eq!(json["write_plane"]["provenance"], "managed_default");
- assert_eq!(
- json["write_plane"]["source"],
- "managed preferred radrootsd instance"
- );
- assert_eq!(json["write_plane"]["target_kind"], "managed_instance");
- assert_eq!(json["write_plane"]["target"], "local");
- assert_eq!(json["write_plane"]["bridge_auth_configured"], true);
-}
-
-#[test]
-fn config_show_rejects_ndjson_for_singular_output() {
- let dir = tempdir().expect("tempdir");
- let output = runtime_show_command_in(dir.path())
- .args(["--ndjson", "config", "show"])
- .output()
- .expect("run config show");
-
- 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("`config show` does not support --ndjson"));
-}
diff --git a/tests/sell.rs b/tests/sell.rs
@@ -1,414 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-use std::sync::{Mutex, MutexGuard, OnceLock};
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_HYF_ENABLED",
- "RADROOTS_HYF_EXECUTABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn sell_test_guard() -> MutexGuard<'static, ()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(()))
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
-}
-
-fn bootstrap_seller(workdir: &Path) {
- let account_output = cli_command_in(workdir)
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
-
- let farm_output = cli_command_in(workdir)
- .args([
- "--json",
- "farm",
- "setup",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- "--city",
- "San Francisco",
- "--region",
- "CA",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ])
- .output()
- .expect("run farm setup");
- assert!(farm_output.status.success());
-}
-
-#[test]
-fn sell_add_creates_named_local_draft_from_human_flags() {
- let _guard = sell_test_guard();
- let dir = tempdir().expect("tempdir");
- bootstrap_seller(dir.path());
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "sell",
- "add",
- "tomatoes",
- "--pack",
- "1 kg",
- "--price",
- "10 USD/kg",
- "--stock",
- "25",
- ])
- .output()
- .expect("run sell add");
- assert!(output.status.success());
-
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "draft_saved");
- assert_eq!(json["product_key"], "tomatoes");
- assert_eq!(json["title"], "Tomatoes");
- assert_eq!(json["offer"], "1 kg");
- assert_eq!(json["price"], "10 USD/kg");
- assert_eq!(json["stock"], "25 available");
- assert_eq!(json["farm_name"], "La Huerta");
- assert_eq!(json["delivery_method"], "pickup");
- assert_eq!(json["location_primary"], "San Francisco, CA");
- assert_eq!(
- json["actions"][0],
- "radroots sell check listing-tomatoes.toml"
- );
- assert_eq!(
- json["actions"][1],
- "radroots sell publish listing-tomatoes.toml"
- );
-
- let draft_path = dir.path().join("listing-tomatoes.toml");
- let contents = fs::read_to_string(&draft_path).expect("draft contents");
- assert!(contents.contains("key = \"tomatoes\""));
- assert!(contents.contains("title = \"Tomatoes\""));
- assert!(contents.contains("category = \"Tomatoes\""));
- assert!(contents.contains("quantity_amount = \"1\""));
- assert!(contents.contains("quantity_unit = \"kg\""));
- assert!(contents.contains("price_amount = \"10\""));
- assert!(contents.contains("price_currency = \"USD\""));
- assert!(contents.contains("price_per_amount = \"1\""));
- assert!(contents.contains("price_per_unit = \"kg\""));
- assert!(contents.contains("available = \"25\""));
-
- let human_output = cli_command_in(dir.path())
- .args([
- "sell", "add", "potatoes", "--pack", "2 kg", "--price", "6 USD/kg", "--stock", "10",
- ])
- .output()
- .expect("run human sell add");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Listing draft saved"));
- assert!(stdout.contains("The draft is local until you publish it."));
- assert!(stdout.contains("Draft"));
- assert!(stdout.contains("Defaults"));
- assert!(stdout.contains("radroots sell check listing-potatoes.toml"));
-}
-
-#[test]
-fn sell_show_reads_local_draft_only() {
- let _guard = sell_test_guard();
- let dir = tempdir().expect("tempdir");
- bootstrap_seller(dir.path());
-
- let add = cli_command_in(dir.path())
- .args([
- "sell",
- "add",
- "tomatoes",
- "--pack",
- "1 kg",
- "--price",
- "10 USD/kg",
- "--stock",
- "25",
- ])
- .output()
- .expect("run sell add");
- assert!(add.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "sell", "show", "listing-tomatoes.toml"])
- .output()
- .expect("run sell show");
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["product_key"], "tomatoes");
- assert_eq!(json["title"], "Tomatoes");
- assert_eq!(json["category"], "Tomatoes");
- assert_eq!(json["offer"], "1 kg");
- assert_eq!(json["price"], "10 USD/kg");
- assert_eq!(json["stock"], "25 available");
- assert_eq!(json["delivery_method"], "pickup");
- assert_eq!(json["location_primary"], "San Francisco, CA");
- assert_eq!(
- json["actions"][0],
- "radroots sell check listing-tomatoes.toml"
- );
-
- let human_output = cli_command_in(dir.path())
- .args(["sell", "show", "listing-tomatoes.toml"])
- .output()
- .expect("run human sell show");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Listing draft"));
- assert!(stdout.contains("tomatoes"));
- assert!(stdout.contains("San Francisco, CA"));
- assert!(!stdout.contains("listing ยท"));
-}
-
-#[test]
-fn sell_check_reports_ready_and_invalid_drafts() {
- let _guard = sell_test_guard();
- let dir = tempdir().expect("tempdir");
- bootstrap_seller(dir.path());
-
- let add = cli_command_in(dir.path())
- .args([
- "sell",
- "add",
- "tomatoes",
- "--pack",
- "1 kg",
- "--price",
- "10 USD/kg",
- "--stock",
- "25",
- ])
- .output()
- .expect("run sell add");
- assert!(add.status.success());
-
- let ready_output = cli_command_in(dir.path())
- .args(["--json", "sell", "check", "listing-tomatoes.toml"])
- .output()
- .expect("run ready sell check");
- assert!(ready_output.status.success());
- let ready_json: Value =
- serde_json::from_slice(ready_output.stdout.as_slice()).expect("ready json");
- assert_eq!(ready_json["state"], "ready");
- assert_eq!(ready_json["valid"], true);
- assert_eq!(ready_json["product_key"], "tomatoes");
- assert_eq!(
- ready_json["actions"][0],
- "radroots sell publish listing-tomatoes.toml"
- );
-
- let draft_path = dir.path().join("listing-tomatoes.toml");
- let broken = fs::read_to_string(&draft_path)
- .expect("draft contents")
- .replace("price_amount = \"10\"", "price_amount = \"\"");
- fs::write(&draft_path, broken).expect("write broken draft");
-
- let invalid_output = cli_command_in(dir.path())
- .args(["sell", "check", "listing-tomatoes.toml"])
- .output()
- .expect("run invalid sell check");
- assert!(invalid_output.status.success());
- let stdout = String::from_utf8(invalid_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Draft needs changes"));
- assert!(stdout.contains("primary_bin.price_amount"));
- assert!(stdout.contains("radroots sell show listing-tomatoes.toml"));
- assert!(stdout.contains("Edit the draft file and run the command again"));
-}
-
-#[test]
-fn sell_reprice_and_restock_mutate_draft_file() {
- let _guard = sell_test_guard();
- let dir = tempdir().expect("tempdir");
- bootstrap_seller(dir.path());
-
- let add = cli_command_in(dir.path())
- .args([
- "sell",
- "add",
- "tomatoes",
- "--pack",
- "1 kg",
- "--price",
- "10 USD/kg",
- "--stock",
- "25",
- ])
- .output()
- .expect("run sell add");
- assert!(add.status.success());
-
- let reprice_output = cli_command_in(dir.path())
- .args([
- "--json",
- "sell",
- "reprice",
- "listing-tomatoes.toml",
- "12 USD/kg",
- ])
- .output()
- .expect("run sell reprice");
- assert!(reprice_output.status.success());
- let reprice_json: Value =
- serde_json::from_slice(reprice_output.stdout.as_slice()).expect("reprice json");
- assert_eq!(reprice_json["state"], "updated");
- assert_eq!(reprice_json["operation"], "reprice");
- assert_eq!(reprice_json["changed_label"], "Price");
- assert_eq!(reprice_json["changed_value"], "12 USD/kg");
-
- let restock_output = cli_command_in(dir.path())
- .args(["--json", "sell", "restock", "listing-tomatoes.toml", "40"])
- .output()
- .expect("run sell restock");
- assert!(restock_output.status.success());
- let restock_json: Value =
- serde_json::from_slice(restock_output.stdout.as_slice()).expect("restock json");
- assert_eq!(restock_json["state"], "updated");
- assert_eq!(restock_json["operation"], "restock");
- assert_eq!(restock_json["changed_label"], "Stock");
- assert_eq!(restock_json["changed_value"], "40 available");
-
- let contents = fs::read_to_string(dir.path().join("listing-tomatoes.toml")).expect("draft");
- assert!(contents.contains("price_amount = \"12\""));
- assert!(contents.contains("available = \"40\""));
-}
-
-#[test]
-fn sell_publish_update_and_pause_wrap_listing_dry_runs() {
- let _guard = sell_test_guard();
- let dir = tempdir().expect("tempdir");
- bootstrap_seller(dir.path());
-
- let add = cli_command_in(dir.path())
- .args([
- "sell",
- "add",
- "tomatoes",
- "--pack",
- "1 kg",
- "--price",
- "10 USD/kg",
- "--stock",
- "25",
- ])
- .output()
- .expect("run sell add");
- assert!(add.status.success());
-
- let publish_output = cli_command_in(dir.path())
- .args([
- "--dry-run",
- "--json",
- "sell",
- "publish",
- "listing-tomatoes.toml",
- ])
- .output()
- .expect("run sell publish dry run");
- assert!(publish_output.status.success());
- let publish_json: Value =
- serde_json::from_slice(publish_output.stdout.as_slice()).expect("publish json");
- assert_eq!(publish_json["state"], "dry_run");
- assert_eq!(publish_json["operation"], "publish");
- assert_eq!(publish_json["product_key"], "tomatoes");
- assert_eq!(
- publish_json["actions"][0],
- "radroots sell publish listing-tomatoes.toml"
- );
-
- let update_output = cli_command_in(dir.path())
- .args([
- "--dry-run",
- "--json",
- "sell",
- "update",
- "listing-tomatoes.toml",
- ])
- .output()
- .expect("run sell update dry run");
- assert!(update_output.status.success());
- let update_json: Value =
- serde_json::from_slice(update_output.stdout.as_slice()).expect("update json");
- assert_eq!(update_json["state"], "dry_run");
- assert_eq!(update_json["operation"], "update");
- assert_eq!(update_json["product_key"], "tomatoes");
- assert_eq!(
- update_json["actions"][0],
- "radroots sell update listing-tomatoes.toml"
- );
-
- let pause_output = cli_command_in(dir.path())
- .args([
- "--dry-run",
- "--json",
- "sell",
- "pause",
- "listing-tomatoes.toml",
- ])
- .output()
- .expect("run sell pause dry run");
- assert!(pause_output.status.success());
- let pause_json: Value =
- serde_json::from_slice(pause_output.stdout.as_slice()).expect("pause json");
- assert_eq!(pause_json["state"], "dry_run");
- assert_eq!(pause_json["operation"], "pause");
- assert_eq!(pause_json["product_key"], "tomatoes");
- assert_eq!(
- pause_json["actions"][0],
- "radroots sell pause listing-tomatoes.toml"
- );
-
- let human_output = cli_command_in(dir.path())
- .args(["--dry-run", "sell", "publish", "listing-tomatoes.toml"])
- .output()
- .expect("run human sell publish dry run");
- assert!(human_output.status.success());
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Dry run only"));
- assert!(stdout.contains("Listing would be published."));
- assert!(stdout.contains("Nothing was written."));
-}
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -1,201 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn signer_status_reports_local_ready_when_account_exists() {
- let dir = tempdir().expect("tempdir");
-
- let init = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(init.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "--signer", "local", "signer", "status"])
- .output()
- .expect("run signer status");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["mode"], "local");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["source"], "shared account store ยท local first");
- assert_eq!(json["signer_account_id"], json["local"]["account_id"]);
- assert_eq!(json["account_resolution"]["source"], "default_account");
- assert_eq!(json["reason"], Value::Null);
- assert_eq!(json["binding"]["state"], "disabled");
- assert_eq!(json["binding"]["source"], "independent local signer mode");
- assert_eq!(json["local"]["availability"], "secret_backed");
- assert_eq!(json["local"]["secret_backed"], true);
- assert_eq!(json["local"]["backend"], "encrypted_file");
- assert_eq!(json["local"]["used_fallback"], true);
-}
-
-#[test]
-fn signer_status_reports_local_unconfigured_when_no_account_is_selected() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "--signer", "local", "signer", "status"])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["mode"], "local");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["binding"]["state"], "disabled");
- assert!(
- json["reason"]
- .as_str()
- .is_some_and(|value| value.contains("no local accounts found"))
- );
- assert_eq!(json["local"], Value::Null);
-}
-
-#[test]
-fn signer_status_reports_internal_error_for_invalid_account_store_file() {
- let dir = tempdir().expect("tempdir");
- let accounts_dir = data_root(dir.path()).join("shared/accounts");
- fs::create_dir_all(&accounts_dir).expect("create accounts dir");
- fs::write(accounts_dir.join("store.json"), "{ not valid json").expect("write invalid store");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "--signer", "local", "signer", "status"])
- .output()
- .expect("run signer status");
-
- assert_eq!(output.status.code(), Some(1));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
- assert_eq!(json["mode"], "local");
- assert_eq!(json["state"], "error");
- assert!(json["reason"].as_str().is_some());
- assert_eq!(json["local"], Value::Null);
-}
-
-#[test]
-fn signer_status_honors_explicit_account_selector_over_default_account() {
- let dir = tempdir().expect("tempdir");
-
- let first = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run first account new");
- assert!(first.status.success());
- let first_json: Value = serde_json::from_slice(first.stdout.as_slice()).expect("first json");
- let first_id = first_json["account"]["id"]
- .as_str()
- .expect("first account id")
- .to_owned();
-
- let second = cli_command_in(dir.path())
- .args(["--json", "account", "new"])
- .output()
- .expect("run second account new");
- assert!(second.status.success());
-
- let output = cli_command_in(dir.path())
- .args([
- "--json",
- "--signer",
- "local",
- "--account",
- first_id.as_str(),
- "signer",
- "status",
- ])
- .output()
- .expect("run signer status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("signer json");
- assert_eq!(json["mode"], "local");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["signer_account_id"], first_id);
- assert_eq!(json["account_resolution"]["source"], "invocation_override");
- assert_eq!(
- json["account_resolution"]["resolved_account"]["id"],
- first_id
- );
- assert_eq!(json["local"]["account_id"], first_id);
- assert_eq!(json["local"]["backend"], "encrypted_file");
- assert_eq!(json["local"]["used_fallback"], true);
-}
-
-#[test]
-fn signer_status_reports_explicit_host_vault_fallback_selection_truthfully() {
- let dir = tempdir().expect("tempdir");
-
- let init = cli_command_in(dir.path())
- .env("RADROOTS_ACCOUNT_SECRET_BACKEND", "host_vault")
- .env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "encrypted_file")
- .args(["--json", "account", "new"])
- .output()
- .expect("run account new");
- assert!(init.status.success());
-
- let output = cli_command_in(dir.path())
- .env("RADROOTS_ACCOUNT_SECRET_BACKEND", "host_vault")
- .env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "encrypted_file")
- .args(["--json", "--signer", "local", "signer", "status"])
- .output()
- .expect("run signer status");
-
- assert!(output.status.success());
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("signer json");
- assert_eq!(json["state"], "ready");
- assert_eq!(json["local"]["backend"], "encrypted_file");
- assert_eq!(json["local"]["used_fallback"], true);
-}
diff --git a/tests/sync.rs b/tests/sync.rs
@@ -1,200 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-#[test]
-fn sync_status_reports_unconfigured_when_local_replica_is_missing() {
- let dir = tempdir().expect("tempdir");
- let output = cli_command_in(dir.path())
- .args(["--json", "sync", "status"])
- .output()
- .expect("run sync status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("sync json");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["replica_db"], "missing");
- assert_eq!(json["freshness"]["display"], "never synced");
- assert_eq!(json["actions"][0], "radroots local init");
-}
-
-#[test]
-fn sync_status_reports_queue_and_relay_setup_need_after_local_init() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "sync", "status"])
- .output()
- .expect("run sync status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("sync json");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["replica_db"], "ready");
- assert_eq!(json["queue"]["pending_count"], 0);
- assert_eq!(json["freshness"]["display"], "never synced");
- assert_eq!(
- json["actions"][0],
- "radroots relay ls --relay wss://relay.example.com"
- );
-}
-
-#[test]
-fn sync_pull_and_push_are_honestly_narrowed_until_relay_plane_lands() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
- let config_dir = dir.path().join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.one\"]\npublish_policy = \"any\"\n",
- )
- .expect("write user config");
-
- let pull = cli_command_in(dir.path())
- .args(["--json", "sync", "pull"])
- .output()
- .expect("run sync pull");
- assert_eq!(pull.status.code(), Some(4));
- let pull_json: Value = serde_json::from_slice(pull.stdout.as_slice()).expect("pull json");
- assert_eq!(pull_json["direction"], "pull");
- assert_eq!(pull_json["state"], "unavailable");
- assert_eq!(pull_json["relay_count"], 1);
- assert!(
- pull_json["reason"]
- .as_str()
- .is_some_and(|reason| reason.contains("relay ingest"))
- );
-
- let push = cli_command_in(dir.path())
- .args(["--json", "sync", "push"])
- .output()
- .expect("run sync push");
- assert_eq!(push.status.code(), Some(4));
- let push_json: Value = serde_json::from_slice(push.stdout.as_slice()).expect("push json");
- assert_eq!(push_json["direction"], "push");
- assert_eq!(push_json["state"], "unavailable");
- assert_eq!(push_json["queue"]["pending_count"], 0);
- assert!(
- push_json["reason"]
- .as_str()
- .is_some_and(|reason| reason.contains("relay publish"))
- );
-}
-
-#[test]
-fn sync_watch_ndjson_emits_one_frame_per_poll() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
- let config_dir = dir.path().join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.one\", \"wss://relay.two\"]\npublish_policy = \"any\"\n",
- )
- .expect("write user config");
-
- let output = cli_command_in(dir.path())
- .args([
- "--ndjson",
- "sync",
- "watch",
- "--frames",
- "2",
- "--interval-ms",
- "1",
- ])
- .output()
- .expect("run sync watch");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- let lines = stdout.lines().collect::<Vec<_>>();
- assert_eq!(lines.len(), 2);
- assert!(lines[0].contains("\"sequence\":1"));
- assert!(lines[0].contains("\"state\":\"ready\""));
- assert!(lines[1].contains("\"sequence\":2"));
- assert!(lines[1].contains("\"relay_count\":2"));
-}
-
-#[test]
-fn sync_watch_human_appends_readable_snapshots_without_screen_clear() {
- let dir = tempdir().expect("tempdir");
- let init = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(init.status.success());
- let config_dir = dir.path().join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(
- config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.one\", \"wss://relay.two\"]\npublish_policy = \"any\"\n",
- )
- .expect("write user config");
-
- let output = cli_command_in(dir.path())
- .args(["sync", "watch", "--frames", "2", "--interval-ms", "1"])
- .output()
- .expect("run human sync watch");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Watching market sync"));
- assert!(stdout.contains("State"));
- assert!(stdout.contains("Ready"));
- assert!(stdout.contains("Relays"));
- assert!(stdout.contains("Queue"));
- assert!(!stdout.contains("activity ยท"));
- assert!(!stdout.contains("\u{1b}"));
-}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -0,0 +1,137 @@
+use std::process::Command;
+
+use assert_cmd::prelude::*;
+use serde_json::Value;
+
+fn radroots() -> Command {
+ Command::cargo_bin("radroots").expect("binary")
+}
+
+#[test]
+fn root_help_exposes_only_mvp_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",
+ "relay",
+ "store",
+ "sync",
+ "runtime",
+ "job",
+ "farm",
+ "listing",
+ "market",
+ "basket",
+ "order",
+ ] {
+ assert!(
+ help_lists(&stdout, namespace),
+ "root help should contain `{namespace}`"
+ );
+ }
+
+ for removed in [
+ "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "product",
+ "message", "approval", "agent",
+ ] {
+ assert!(
+ !help_lists(&stdout, removed),
+ "root help should not contain `{removed}`"
+ );
+ }
+}
+
+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(),
+ ] {
+ 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 removed_command_families_are_rejected_publicly() {
+ for command in [
+ "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "product",
+ "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 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 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 value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
+
+ assert_eq!(value["operation_id"], "workspace.get");
+ assert_eq!(value["errors"][0]["code"], "invalid_input");
+ assert_eq!(value["errors"][0]["exit_code"], 2);
+}
+
+#[test]
+fn required_approval_missing_token_returns_structured_error() {
+ let output = radroots()
+ .args(["--format", "json", "order", "submit"])
+ .output()
+ .expect("run order submit");
+
+ assert_eq!(output.status.code(), Some(6));
+ let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
+
+ assert_eq!(value["operation_id"], "order.submit");
+ assert_eq!(value["errors"][0]["code"], "approval_required");
+ assert_eq!(value["errors"][0]["exit_code"], 6);
+}
diff --git a/tests/workflow.rs b/tests/workflow.rs
@@ -1,274 +0,0 @@
-use std::fs;
-use std::path::Path;
-use std::process::Command;
-
-use assert_cmd::prelude::*;
-use serde_json::Value;
-use tempfile::tempdir;
-
-fn data_root(workdir: &Path) -> std::path::PathBuf {
- if cfg!(windows) {
- workdir.join("local").join("Radroots").join("data")
- } else {
- workdir.join("home").join(".radroots").join("data")
- }
-}
-
-fn cli_command_in(workdir: &Path) -> Command {
- let mut command = Command::cargo_bin("radroots").expect("binary");
- command.current_dir(workdir);
- command.env("HOME", workdir.join("home"));
- command.env("APPDATA", workdir.join("roaming"));
- command.env("LOCALAPPDATA", workdir.join("local"));
- for key in [
- "RADROOTS_ENV_FILE",
- "RADROOTS_OUTPUT",
- "RADROOTS_CLI_LOGGING_FILTER",
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR",
- "RADROOTS_CLI_LOGGING_STDOUT",
- "RADROOTS_CLI_PATHS_PROFILE",
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT",
- "RADROOTS_LOG_FILTER",
- "RADROOTS_LOG_DIR",
- "RADROOTS_LOG_STDOUT",
- "RADROOTS_ACCOUNT",
- "RADROOTS_ACCOUNT_SECRET_BACKEND",
- "RADROOTS_ACCOUNT_SECRET_FALLBACK",
- "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE",
- "RADROOTS_IDENTITY_PATH",
- "RADROOTS_SIGNER",
- "RADROOTS_RELAYS",
- "RADROOTS_MYC_EXECUTABLE",
- "RADROOTS_MYC_STATUS_TIMEOUT_MS",
- "RADROOTS_RPC_URL",
- "RADROOTS_RPC_BEARER_TOKEN",
- ] {
- command.env_remove(key);
- }
- command.env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false");
- command
-}
-
-fn write_user_config(workdir: &Path, contents: &str) {
- let config_dir = workdir.join("home/.radroots/config/apps/cli");
- fs::create_dir_all(&config_dir).expect("user config dir");
- fs::write(config_dir.join("config.toml"), contents).expect("write user config");
-}
-
-#[test]
-fn setup_seller_without_account_is_unconfigured_and_does_not_create_account() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let output = cli_command_in(dir.path())
- .args(["setup", "seller"])
- .output()
- .expect("run setup seller");
-
- assert_eq!(output.status.code(), Some(3));
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Not ready yet"));
- assert!(stdout.contains("Ready"));
- assert!(stdout.contains("Resolved account"));
- assert!(stdout.contains("Local market data"));
- assert!(stdout.contains("Needs attention"));
- assert!(stdout.contains("Relay configuration"));
- assert!(stdout.contains("Account resolution"));
- assert!(stdout.contains("radroots account create"));
- assert!(stdout.contains("radroots setup seller"));
- assert!(!store_path.exists());
-
- let local_output = cli_command_in(dir.path())
- .args(["--json", "local", "status"])
- .output()
- .expect("run local status");
- assert!(local_output.status.success());
- let local_json: Value =
- serde_json::from_slice(local_output.stdout.as_slice()).expect("local json");
- assert_eq!(local_json["state"], "ready");
-}
-
-#[test]
-fn setup_seller_with_default_account_reports_farm_attention() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let account_output = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- assert!(store_path.exists());
-
- let output = cli_command_in(dir.path())
- .args(["setup", "seller"])
- .output()
- .expect("run setup seller");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Setup saved"));
- assert!(stdout.contains("Ready"));
- assert!(stdout.contains("Resolved account"));
- assert!(stdout.contains("Local market data"));
- assert!(stdout.contains("Needs attention"));
- assert!(stdout.contains("Relay configuration"));
- assert!(stdout.contains("Farm draft"));
- assert!(stdout.contains("Account resolution"));
- assert!(stdout.contains("radroots farm init"));
- assert!(stdout.contains("radroots status"));
-}
-
-#[test]
-fn status_is_unconfigured_before_account_setup() {
- let dir = tempdir().expect("tempdir");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "status"])
- .output()
- .expect("run status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("status json");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["ready"], Value::Array(Vec::new()));
- assert_eq!(
- json["needs_attention"],
- serde_json::json!([
- "Resolved account",
- "Local market data",
- "Relay configuration"
- ])
- );
- assert_eq!(json["next"], serde_json::json!(["radroots account create"]));
-}
-
-#[test]
-fn status_points_to_account_selection_when_accounts_exist_without_default() {
- let dir = tempdir().expect("tempdir");
- let store_path = data_root(dir.path()).join("shared/accounts/store.json");
-
- let account_output = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account_output.status.success());
- let mut store_json: Value =
- serde_json::from_slice(fs::read(&store_path).expect("read store").as_slice())
- .expect("parse store");
- store_json["default_account_id"] = Value::Null;
- fs::write(
- &store_path,
- serde_json::to_vec_pretty(&store_json).expect("serialize store"),
- )
- .expect("write store");
-
- let output = cli_command_in(dir.path())
- .args(["--json", "status"])
- .output()
- .expect("run status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("status json");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["account_resolution"]["source"], "none");
- assert_eq!(
- json["next"],
- serde_json::json!([
- "radroots account list",
- "radroots account select <selector>"
- ])
- );
-}
-
-#[test]
-fn status_calls_out_missing_relay_after_buyer_setup() {
- let dir = tempdir().expect("tempdir");
- let account = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account.status.success());
- let setup = cli_command_in(dir.path())
- .args(["setup", "buyer"])
- .output()
- .expect("run setup buyer");
- assert!(setup.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["--json", "status"])
- .output()
- .expect("run status");
-
- assert_eq!(output.status.code(), Some(3));
- let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("status json");
- assert_eq!(json["state"], "unconfigured");
- assert_eq!(
- json["ready"],
- serde_json::json!(["Resolved account", "Local market data"])
- );
- assert_eq!(
- json["needs_attention"],
- serde_json::json!(["Relay configuration"])
- );
- assert_eq!(
- json["next"],
- serde_json::json!([
- "radroots relay list --relay wss://relay.example.com",
- "radroots status"
- ])
- );
-}
-
-#[test]
-fn status_reports_farm_publish_need_when_core_state_is_ready() {
- let dir = tempdir().expect("tempdir");
-
- let account = cli_command_in(dir.path())
- .args(["account", "new"])
- .output()
- .expect("run account new");
- assert!(account.status.success());
-
- let local = cli_command_in(dir.path())
- .args(["local", "init"])
- .output()
- .expect("run local init");
- assert!(local.status.success());
-
- write_user_config(
- dir.path(),
- "[relay]\nurls = [\"wss://relay.one\"]\npublish_policy = \"any\"\n",
- );
-
- let farm = cli_command_in(dir.path())
- .args([
- "farm",
- "setup",
- "--name",
- "La Huerta",
- "--location",
- "San Francisco, CA",
- ])
- .output()
- .expect("run farm setup");
- assert!(farm.status.success());
-
- let output = cli_command_in(dir.path())
- .args(["status"])
- .output()
- .expect("run status");
-
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("Status"));
- assert!(stdout.contains("Ready"));
- assert!(stdout.contains("Resolved account"));
- assert!(stdout.contains("Account resolution"));
- assert!(stdout.contains("Local market data"));
- assert!(stdout.contains("Relay configuration"));
- assert!(stdout.contains("Needs attention"));
- assert!(stdout.contains("Farm not yet published"));
- assert!(stdout.contains("radroots farm publish"));
- assert!(stdout.contains("radroots sell add tomatoes"));
-}