cli

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

commit 39d391e2bec5e3fad32b2d1b8e82a5753622cc91
parent d7b6a67dc1b5532371760afc2c7da566dd50b35a
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Apr 2026 04:07:48 +0000

reconcile signer operator surfaces

Diffstat:
Msrc/cli.rs | 6+++---
Msrc/commands/doctor.rs | 14+++++---------
Msrc/commands/runtime.rs | 2+-
Msrc/domain/runtime.rs | 12++++++++++--
Msrc/render/mod.rs | 62+++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/runtime/config.rs | 22+++++++++++-----------
Msrc/runtime/myc.rs | 2++
Msrc/runtime/signer.rs | 23++++++++++++++++++-----
Mtests/doctor.rs | 4++--
Mtests/identity_commands.rs | 2+-
Mtests/myc_status.rs | 12+++++++++---
Mtests/runtime_show.rs | 8++++----
Mtests/signer_status.rs | 58+++++++++++++++++++++++++++++++++++++++++++++++++++-------
13 files changed, 160 insertions(+), 67 deletions(-)

diff --git a/src/cli.rs b/src/cli.rs @@ -36,7 +36,7 @@ pub struct CliArgs { #[arg(long, global = true)] pub identity_path: Option<PathBuf>, #[arg(long, global = true)] - pub signer_backend: Option<String>, + pub signer: Option<String>, #[arg(long, global = true)] pub myc_executable: Option<PathBuf>, #[command(subcommand)] @@ -381,7 +381,7 @@ mod tests { "--log-stdout", "--identity-path", "identity.local.json", - "--signer-backend", + "--signer", "myc", "--myc-executable", "bin/myc", @@ -413,7 +413,7 @@ mod tests { .and_then(|path| path.to_str()), Some("identity.local.json") ); - assert_eq!(parsed.signer_backend.as_deref(), Some("myc")); + assert_eq!(parsed.signer.as_deref(), Some("myc")); assert_eq!( parsed .myc_executable diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs @@ -160,17 +160,13 @@ fn account_check(config: &RuntimeConfig) -> Result<EvaluatedCheck, RuntimeError> fn signer_check(signer: &crate::domain::runtime::SignerStatusView) -> EvaluatedCheck { let (severity, detail, action) = match signer.state.as_str() { - "ready" => ( - DoctorSeverity::Ok, - format!("{} ready", signer.backend), - None, - ), + "ready" => (DoctorSeverity::Ok, format!("{} ready", signer.mode), None), "unconfigured" => ( DoctorSeverity::Warn, signer .reason .clone() - .unwrap_or_else(|| format!("{} signer is not configured", signer.backend)), + .unwrap_or_else(|| format!("{} signer is not configured", signer.mode)), Some("radroots signer status"), ), "degraded" | "unavailable" => ( @@ -178,8 +174,8 @@ fn signer_check(signer: &crate::domain::runtime::SignerStatusView) -> EvaluatedC signer .reason .clone() - .unwrap_or_else(|| format!("{} signer is unavailable", signer.backend)), - Some(if signer.backend == "myc" { + .unwrap_or_else(|| format!("{} signer is unavailable", signer.mode)), + Some(if signer.mode == "myc" { "radroots myc status" } else { "radroots signer status" @@ -190,7 +186,7 @@ fn signer_check(signer: &crate::domain::runtime::SignerStatusView) -> EvaluatedC signer .reason .clone() - .unwrap_or_else(|| format!("{} signer reported an internal error", signer.backend)), + .unwrap_or_else(|| format!("{} signer reported an internal error", signer.mode)), Some("radroots signer status --json"), ), }; diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs @@ -44,7 +44,7 @@ pub fn show(config: &RuntimeConfig, logging: &LoggingState) -> ConfigShowView { legacy_identity_path: config.identity.path.display().to_string(), }, signer: SignerRuntimeView { - backend: config.signer.backend.as_str().to_owned(), + mode: config.signer.backend.as_str().to_owned(), }, myc: MycRuntimeView { executable: config.myc.executable.display().to_string(), diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -131,7 +131,7 @@ pub struct AccountRuntimeView { #[derive(Debug, Clone, Serialize)] pub struct SignerRuntimeView { - pub backend: String, + pub mode: String, } #[derive(Debug, Clone, Serialize)] @@ -247,10 +247,16 @@ pub struct AccountListView { #[derive(Debug, Clone, Serialize)] pub struct SignerStatusView { - pub backend: String, + pub mode: String, pub state: String, + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] pub local: Option<LocalSignerStatusView>, + #[serde(skip_serializing_if = "Option::is_none")] pub myc: Option<MycStatusView>, } @@ -278,8 +284,10 @@ pub struct LocalSignerStatusView { pub struct MycStatusView { pub executable: String, pub state: String, + pub source: String, pub service_status: Option<String>, pub ready: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub reasons: Vec<String>, diff --git a/src/render/mod.rs b/src/render/mod.rs @@ -69,7 +69,7 @@ fn render_human_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), writeln!(stdout, "source: {}", view.source)?; } CommandView::MycStatus(view) => { - render_myc_status(stdout, view)?; + render_myc_status(stdout, view, true)?; } CommandView::ConfigShow(view) => { render_config_show(stdout, view)?; @@ -78,17 +78,35 @@ fn render_human_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), render_doctor(stdout, view)?; } CommandView::SignerStatus(view) => { - writeln!(stdout, "signer")?; - writeln!(stdout, " backend: {}", view.backend)?; - writeln!(stdout, " state: {}", view.state)?; + write_context( + stdout, + match view.state.as_str() { + "ready" => "signer · active", + "unconfigured" => "signer · unconfigured", + "degraded" => "signer · degraded", + "unavailable" => "signer · unavailable", + _ => "signer · error", + }, + )?; + let mut signer_rows = vec![ + ("mode", view.mode.as_str()), + ("status", view.state.as_str()), + ]; + if let Some(account_id) = &view.account_id { + signer_rows.push(("account id", account_id.as_str())); + } + render_pairs(stdout, "signer", signer_rows.as_slice())?; if let Some(reason) = &view.reason { - writeln!(stdout, " reason: {reason}")?; + writeln!(stdout, "reason: {reason}")?; } + writeln!(stdout, "source: {}", view.source)?; if let Some(local) = &view.local { - render_local_signer(stdout, "local signer", local)?; + writeln!(stdout)?; + render_local_signer(stdout, "local account", local)?; } if let Some(myc) = &view.myc { - render_myc_status(stdout, myc)?; + writeln!(stdout)?; + render_myc_status(stdout, myc, false)?; } } } @@ -255,11 +273,7 @@ fn render_config_show( account_rows.insert(0, ("selector", selector.as_str())); } render_pairs(stdout, "account", account_rows.as_slice())?; - render_pairs( - stdout, - "signer", - &[("backend", view.signer.backend.as_str())], - )?; + render_pairs(stdout, "signer", &[("mode", view.signer.mode.as_str())])?; render_pairs( stdout, "myc", @@ -388,24 +402,33 @@ fn render_local_signer( fn render_myc_status( stdout: &mut dyn Write, view: &crate::domain::runtime::MycStatusView, + standalone: bool, ) -> Result<(), RuntimeError> { - writeln!(stdout, "myc")?; - writeln!(stdout, " executable: {}", view.executable)?; - writeln!(stdout, " state: {}", view.state)?; - writeln!(stdout, " ready: {}", yes_no(view.ready))?; + if standalone { + write_context(stdout, format!("myc · {}", view.state).as_str())?; + } + let mut rows = vec![ + ("executable", view.executable.as_str()), + ("status", view.state.as_str()), + ("ready", yes_no(view.ready)), + ]; if let Some(service_status) = &view.service_status { - writeln!(stdout, " service status: {service_status}")?; + rows.push(("service status", service_status.as_str())); } + render_pairs(stdout, "myc", rows.as_slice())?; if let Some(reason) = &view.reason { - writeln!(stdout, " reason: {reason}")?; + writeln!(stdout, "reason: {reason}")?; } if !view.reasons.is_empty() { - writeln!(stdout, " reasons: {}", view.reasons.join(" | "))?; + writeln!(stdout, "reasons: {}", view.reasons.join(" | "))?; } + writeln!(stdout, "source: {}", view.source)?; if let Some(local_signer) = &view.local_signer { + writeln!(stdout)?; render_local_signer(stdout, "myc local signer", local_signer)?; } if let Some(custody) = &view.custody { + writeln!(stdout)?; render_myc_custody_identity(stdout, "myc custody signer", &custody.signer)?; render_myc_custody_identity(stdout, "myc custody user", &custody.user)?; if let Some(discovery_app) = &custody.discovery_app { @@ -560,6 +583,7 @@ mod tests { let output = CommandOutput::success(CommandView::MycStatus(MycStatusView { executable: "myc".to_owned(), state: "unavailable".to_owned(), + source: "myc status command · local first".to_owned(), service_status: None, ready: false, reason: None, diff --git a/src/runtime/config.rs b/src/runtime/config.rs @@ -21,7 +21,7 @@ const ENV_LOG_DIR: &str = "RADROOTS_LOG_DIR"; const ENV_LOG_STDOUT: &str = "RADROOTS_LOG_STDOUT"; const ENV_ACCOUNT: &str = "RADROOTS_ACCOUNT"; const ENV_IDENTITY_PATH: &str = "RADROOTS_IDENTITY_PATH"; -const ENV_SIGNER_BACKEND: &str = "RADROOTS_SIGNER_BACKEND"; +const ENV_SIGNER: &str = "RADROOTS_SIGNER"; const ENV_MYC_EXECUTABLE: &str = "RADROOTS_MYC_EXECUTABLE"; const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[ ENV_OUTPUT, @@ -33,7 +33,7 @@ const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[ ENV_LOG_STDOUT, ENV_ACCOUNT, ENV_IDENTITY_PATH, - ENV_SIGNER_BACKEND, + ENV_SIGNER, ENV_MYC_EXECUTABLE, ]; @@ -229,10 +229,10 @@ impl RuntimeConfig { }, signer: SignerConfig { backend: args - .signer_backend + .signer .clone() - .or_else(|| env_value(env, env_file, &[ENV_SIGNER_BACKEND])) - .map(parse_signer_backend) + .or_else(|| env_value(env, env_file, &[ENV_SIGNER])) + .map(parse_signer_mode) .transpose()? .unwrap_or(SignerBackend::Local), }, @@ -428,12 +428,12 @@ fn parse_output_format(value: &str) -> Result<OutputFormat, RuntimeError> { } } -fn parse_signer_backend(value: String) -> Result<SignerBackend, RuntimeError> { +fn parse_signer_mode(value: String) -> Result<SignerBackend, RuntimeError> { match value.trim().to_ascii_lowercase().as_str() { "local" => Ok(SignerBackend::Local), "myc" => Ok(SignerBackend::Myc), other => Err(RuntimeError::Config(format!( - "{ENV_SIGNER_BACKEND} or --signer-backend must be `local` or `myc`, got `{other}`" + "{ENV_SIGNER} or --signer must be `local` or `myc`, got `{other}`" ))), } } @@ -502,7 +502,7 @@ mod tests { "--log-stdout", "--identity-path", "custom-identity.json", - "--signer-backend", + "--signer", "local", "--myc-executable", "bin/myc-cli", @@ -517,7 +517,7 @@ mod tests { "RADROOTS_IDENTITY_PATH".to_owned(), "env-identity.json".to_owned(), ), - ("RADROOTS_SIGNER_BACKEND".to_owned(), "myc".to_owned()), + ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()), ("RADROOTS_MYC_EXECUTABLE".to_owned(), "env-myc".to_owned()), ])); @@ -576,7 +576,7 @@ mod tests { "RADROOTS_IDENTITY_PATH".to_owned(), "state/identity.json".to_owned(), ), - ("RADROOTS_SIGNER_BACKEND".to_owned(), "myc".to_owned()), + ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()), ("RADROOTS_MYC_EXECUTABLE".to_owned(), "bin/myc".to_owned()), ])); @@ -671,7 +671,7 @@ RADROOTS_CLI_LOGGING_OUTPUT_DIR=/tmp/radroots-cli-logs RADROOTS_CLI_LOGGING_STDOUT=false RADROOTS_ACCOUNT=acct_env_file RADROOTS_IDENTITY_PATH=state/identity.json -RADROOTS_SIGNER_BACKEND=myc +RADROOTS_SIGNER=myc RADROOTS_MYC_EXECUTABLE=bin/myc "#, Path::new(".env.test"), diff --git a/src/runtime/myc.rs b/src/runtime/myc.rs @@ -126,6 +126,7 @@ pub fn resolve_status(config: &MycConfig) -> MycStatusView { MycStatusView { executable, state: state.to_owned(), + source: "myc status command · local first".to_owned(), service_status: Some(payload.status), ready: payload.ready, reason, @@ -220,6 +221,7 @@ fn unavailable_status(executable: String, state: &str, reason: String) -> MycSta MycStatusView { executable, state: state.to_owned(), + source: "myc status command · local first".to_owned(), service_status: None, ready: false, reason: Some(reason), diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -28,8 +28,10 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { .expect("local signer capability") .clone(); SignerStatusView { - backend: config.signer.backend.as_str().to_owned(), + mode: config.signer.backend.as_str().to_owned(), state: "ready".to_owned(), + source: "local account store · local first".to_owned(), + account_id: Some(local.account_id.to_string()), reason: None, local: Some(LocalSignerStatusView { account_id: local.account_id.to_string(), @@ -43,8 +45,10 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { } } Ok(RadrootsNostrSelectedAccountStatus::PublicOnly { account }) => SignerStatusView { - backend: config.signer.backend.as_str().to_owned(), + mode: config.signer.backend.as_str().to_owned(), state: "unconfigured".to_owned(), + source: "local account store · local first".to_owned(), + account_id: Some(account.account_id.to_string()), reason: Some(format!( "local account {} is present but not secret-backed", account.account_id @@ -59,8 +63,10 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { myc: None, }, Ok(RadrootsNostrSelectedAccountStatus::NotConfigured) => SignerStatusView { - backend: config.signer.backend.as_str().to_owned(), + mode: config.signer.backend.as_str().to_owned(), state: "unconfigured".to_owned(), + source: "local account store · local first".to_owned(), + account_id: None, reason: Some(format!( "no local account is selected in {}", config.account.store_path.display() @@ -69,8 +75,10 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { myc: None, }, Err(error) => SignerStatusView { - backend: config.signer.backend.as_str().to_owned(), + mode: config.signer.backend.as_str().to_owned(), state: "error".to_owned(), + source: "local account store · local first".to_owned(), + account_id: None, reason: Some(error.to_string()), local: None, myc: None, @@ -81,8 +89,13 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { fn resolve_myc_signer_status(config: &RuntimeConfig) -> SignerStatusView { let myc = crate::runtime::myc::resolve_status(&config.myc); SignerStatusView { - backend: config.signer.backend.as_str().to_owned(), + mode: config.signer.backend.as_str().to_owned(), state: myc.state.clone(), + source: "myc status command · local first".to_owned(), + account_id: myc + .local_signer + .as_ref() + .map(|local| local.account_id.clone()), reason: myc.reason.clone(), local: None, myc: Some(myc), diff --git a/tests/doctor.rs b/tests/doctor.rs @@ -21,7 +21,7 @@ fn doctor_command_in(workdir: &Path) -> Command { "RADROOTS_LOG_STDOUT", "RADROOTS_ACCOUNT", "RADROOTS_IDENTITY_PATH", - "RADROOTS_SIGNER_BACKEND", + "RADROOTS_SIGNER", "RADROOTS_MYC_EXECUTABLE", ] { command.env_remove(key); @@ -81,7 +81,7 @@ fn doctor_reports_ready_local_bootstrap_state() { #[test] fn doctor_reports_external_failure_for_missing_myc() { let dir = tempdir().expect("tempdir"); - fs::write(dir.path().join(".env"), "RADROOTS_SIGNER_BACKEND=myc\n").expect("write env file"); + fs::write(dir.path().join(".env"), "RADROOTS_SIGNER=myc\n").expect("write env file"); let output = doctor_command_in(dir.path()) .args(["--json", "--myc-executable", "missing-myc", "doctor"]) diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs @@ -20,7 +20,7 @@ fn cli_command_in(workdir: &Path) -> Command { "RADROOTS_LOG_STDOUT", "RADROOTS_ACCOUNT", "RADROOTS_IDENTITY_PATH", - "RADROOTS_SIGNER_BACKEND", + "RADROOTS_SIGNER", "RADROOTS_MYC_EXECUTABLE", ] { command.env_remove(key); diff --git a/tests/myc_status.rs b/tests/myc_status.rs @@ -24,7 +24,7 @@ fn cli_command_in(workdir: &Path) -> Command { "RADROOTS_LOG_STDOUT", "RADROOTS_ACCOUNT", "RADROOTS_IDENTITY_PATH", - "RADROOTS_SIGNER_BACKEND", + "RADROOTS_SIGNER", "RADROOTS_MYC_EXECUTABLE", ] { command.env_remove(key); @@ -56,6 +56,7 @@ fn myc_status_reports_ready_for_valid_full_status_payload() { 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["local_signer"]["availability"], "secret_backed"); @@ -140,7 +141,7 @@ fn signer_status_reports_degraded_myc_backend_as_external_unavailable() { let output = cli_command_in(dir.path()) .args([ "--json", - "--signer-backend", + "--signer", "myc", "--myc-executable", executable.to_str().expect("executable path"), @@ -153,8 +154,13 @@ fn signer_status_reports_degraded_myc_backend_as_external_unavailable() { 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["backend"], "myc"); + assert_eq!(json["mode"], "myc"); assert_eq!(json["state"], "degraded"); + assert_eq!(json["source"], "myc status command · local first"); + assert_eq!( + json["account_id"], + json["myc"]["local_signer"]["account_id"] + ); assert_eq!(json["myc"]["state"], "degraded"); assert_eq!(json["myc"]["service_status"], "degraded"); assert!( diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs @@ -21,7 +21,7 @@ fn runtime_show_command_in(workdir: &Path) -> Command { "RADROOTS_LOG_STDOUT", "RADROOTS_ACCOUNT", "RADROOTS_IDENTITY_PATH", - "RADROOTS_SIGNER_BACKEND", + "RADROOTS_SIGNER", "RADROOTS_MYC_EXECUTABLE", ] { command.env_remove(key); @@ -92,7 +92,7 @@ fn config_show_json_reports_default_bootstrap_state() { .to_string() ); assert_eq!(json["account"]["legacy_identity_path"], "identity.json"); - assert_eq!(json["signer"]["backend"], "local"); + assert_eq!(json["signer"]["mode"], "local"); assert_eq!(json["myc"]["executable"], "myc"); } @@ -106,7 +106,7 @@ fn config_show_json_reflects_environment_configuration() { .env("RADROOTS_LOG_STDOUT", "false") .env("RADROOTS_ACCOUNT", "acct_demo") .env("RADROOTS_IDENTITY_PATH", "state/identity.json") - .env("RADROOTS_SIGNER_BACKEND", "myc") + .env("RADROOTS_SIGNER", "myc") .env("RADROOTS_MYC_EXECUTABLE", "bin/myc") .args(["config", "show"]) .output() @@ -123,7 +123,7 @@ fn config_show_json_reflects_environment_configuration() { json["account"]["legacy_identity_path"], "state/identity.json" ); - assert_eq!(json["signer"]["backend"], "myc"); + assert_eq!(json["signer"]["mode"], "myc"); assert_eq!(json["myc"]["executable"], "bin/myc"); } diff --git a/tests/signer_status.rs b/tests/signer_status.rs @@ -21,7 +21,7 @@ fn cli_command_in(workdir: &Path) -> Command { "RADROOTS_LOG_STDOUT", "RADROOTS_ACCOUNT", "RADROOTS_IDENTITY_PATH", - "RADROOTS_SIGNER_BACKEND", + "RADROOTS_SIGNER", "RADROOTS_MYC_EXECUTABLE", ] { command.env_remove(key); @@ -40,15 +40,17 @@ fn signer_status_reports_local_ready_when_account_exists() { assert!(init.status.success()); let output = cli_command_in(dir.path()) - .args(["--json", "--signer-backend", "local", "signer", "status"]) + .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["backend"], "local"); + assert_eq!(json["mode"], "local"); assert_eq!(json["state"], "ready"); + assert_eq!(json["source"], "local account store · local first"); + assert_eq!(json["account_id"], json["local"]["account_id"]); assert_eq!(json["reason"], Value::Null); assert_eq!(json["local"]["availability"], "secret_backed"); assert_eq!(json["local"]["secret_backed"], true); @@ -59,14 +61,14 @@ 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-backend", "local", "signer", "status"]) + .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["backend"], "local"); + assert_eq!(json["mode"], "local"); assert_eq!(json["state"], "unconfigured"); assert!( json["reason"] @@ -84,15 +86,57 @@ fn signer_status_reports_internal_error_for_invalid_account_store_file() { fs::write(accounts_dir.join("store.json"), "{ not valid json").expect("write invalid store"); let output = cli_command_in(dir.path()) - .args(["--json", "--signer-backend", "local", "signer", "status"]) + .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["backend"], "local"); + 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["account_id"], first_id); + assert_eq!(json["local"]["account_id"], first_id); +}