cli

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

commit 9c18ca6145da99a828a29b39fdce53b3a5e3d2f5
parent a86cae13aad8bac92cb28f55c01e94c11ec5cf88
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Apr 2026 03:22:30 +0000

land config and doctor operator entrypoints

Diffstat:
Asrc/commands/doctor.rs | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/commands/mod.rs | 3++-
Msrc/commands/runtime.rs | 9+++++++--
Msrc/domain/runtime.rs | 26++++++++++++++++++++++++++
Msrc/render/mod.rs | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Atests/doctor.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/runtime_show.rs | 3+++
7 files changed, 569 insertions(+), 39 deletions(-)

diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs @@ -0,0 +1,254 @@ +use crate::domain::runtime::{ + CommandDisposition, CommandOutput, CommandView, DoctorCheckView, DoctorView, +}; +use crate::runtime::config::{RuntimeConfig, SignerBackend}; +use crate::runtime::logging::LoggingState; +use crate::runtime::signer::resolve_signer_status; +use radroots_identity::IdentityError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum DoctorSeverity { + Ok, + Warn, + ExternalFail, + InternalFail, +} + +impl DoctorSeverity { + fn status(self) -> &'static str { + match self { + Self::Ok => "ok", + Self::Warn => "warn", + Self::ExternalFail | Self::InternalFail => "fail", + } + } + + fn command_disposition(self) -> CommandDisposition { + match self { + Self::Ok => CommandDisposition::Success, + Self::Warn => CommandDisposition::Unconfigured, + Self::ExternalFail => CommandDisposition::ExternalUnavailable, + Self::InternalFail => CommandDisposition::InternalError, + } + } +} + +struct EvaluatedCheck { + severity: DoctorSeverity, + view: DoctorCheckView, + action: Option<&'static str>, +} + +pub fn report(config: &RuntimeConfig, logging: &LoggingState) -> CommandOutput { + let mut checks = Vec::new(); + checks.push(config_check(config)); + checks.push(account_check(config)); + + let signer = resolve_signer_status(config); + checks.push(signer_check(&signer)); + + if matches!(config.signer.backend, SignerBackend::Myc) { + if let Some(myc) = signer.myc.as_ref() { + checks.push(myc_check(myc)); + } + } + + checks.push(logging_check(config, logging)); + + let severity = checks + .iter() + .map(|check| check.severity) + .max() + .unwrap_or(DoctorSeverity::Ok); + let actions = collect_actions(&checks); + let view = DoctorView { + ok: severity == DoctorSeverity::Ok, + state: severity.status().to_owned(), + checks: checks.into_iter().map(|check| check.view).collect(), + source: match config.signer.backend { + SignerBackend::Local => "local diagnostics".to_owned(), + SignerBackend::Myc => "local diagnostics + myc status command".to_owned(), + }, + actions, + }; + + match severity.command_disposition() { + CommandDisposition::Success => CommandOutput::success(CommandView::Doctor(view)), + CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::Doctor(view)), + CommandDisposition::ExternalUnavailable => { + CommandOutput::external_unavailable(CommandView::Doctor(view)) + } + CommandDisposition::InternalError => { + CommandOutput::internal_error(CommandView::Doctor(view)) + } + } +} + +fn config_check(config: &RuntimeConfig) -> EvaluatedCheck { + let detail = match ( + config.paths.user_config_path.exists(), + config.paths.workspace_config_path.exists(), + ) { + (false, false) => "defaults active".to_owned(), + (true, false) => "user config root present".to_owned(), + (false, true) => "workspace config root present".to_owned(), + (true, true) => "user and workspace config roots present".to_owned(), + }; + + EvaluatedCheck { + severity: DoctorSeverity::Ok, + view: DoctorCheckView { + name: "config".to_owned(), + status: "ok".to_owned(), + detail, + }, + action: None, + } +} + +fn account_check(config: &RuntimeConfig) -> EvaluatedCheck { + match crate::runtime::identity::load_identity(&config.identity) { + Ok(identity) => EvaluatedCheck { + severity: DoctorSeverity::Ok, + view: DoctorCheckView { + name: "account".to_owned(), + status: "ok".to_owned(), + detail: format!("{} loaded", identity.public_identity.id), + }, + action: None, + }, + Err(crate::runtime::RuntimeError::Identity(IdentityError::NotFound(path))) => { + EvaluatedCheck { + severity: DoctorSeverity::Warn, + view: DoctorCheckView { + name: "account".to_owned(), + status: "warn".to_owned(), + detail: format!("no local account at {}", path.display()), + }, + action: Some("radroots account new"), + } + } + Err(error) => EvaluatedCheck { + severity: DoctorSeverity::InternalFail, + view: DoctorCheckView { + name: "account".to_owned(), + status: "fail".to_owned(), + detail: error.to_string(), + }, + action: Some("radroots account whoami --json"), + }, + } +} + +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, + ), + "unconfigured" => ( + DoctorSeverity::Warn, + signer + .reason + .clone() + .unwrap_or_else(|| format!("{} signer is not configured", signer.backend)), + Some("radroots signer status"), + ), + "degraded" | "unavailable" => ( + DoctorSeverity::ExternalFail, + signer + .reason + .clone() + .unwrap_or_else(|| format!("{} signer is unavailable", signer.backend)), + Some(if signer.backend == "myc" { + "radroots myc status" + } else { + "radroots signer status" + }), + ), + _ => ( + DoctorSeverity::InternalFail, + signer + .reason + .clone() + .unwrap_or_else(|| format!("{} signer reported an internal error", signer.backend)), + Some("radroots signer status --json"), + ), + }; + + EvaluatedCheck { + severity, + view: DoctorCheckView { + name: "signer".to_owned(), + status: severity.status().to_owned(), + detail, + }, + action, + } +} + +fn myc_check(myc: &crate::domain::runtime::MycStatusView) -> EvaluatedCheck { + let (severity, detail, action) = match myc.state.as_str() { + "ready" => ( + DoctorSeverity::Ok, + myc.service_status + .clone() + .unwrap_or_else(|| "service ready".to_owned()), + None, + ), + "unconfigured" => ( + DoctorSeverity::Warn, + myc.reason + .clone() + .unwrap_or_else(|| "myc is not configured".to_owned()), + Some("radroots myc status"), + ), + _ => ( + DoctorSeverity::ExternalFail, + myc.reason + .clone() + .unwrap_or_else(|| "myc is unavailable".to_owned()), + Some("radroots myc status"), + ), + }; + + EvaluatedCheck { + severity, + view: DoctorCheckView { + name: "myc".to_owned(), + status: severity.status().to_owned(), + detail, + }, + action, + } +} + +fn logging_check(config: &RuntimeConfig, logging: &LoggingState) -> EvaluatedCheck { + let detail = match (config.logging.stdout, logging.current_file.as_ref()) { + (true, Some(path)) => format!("stdout + file {}", path.display()), + (true, None) => "stdout only".to_owned(), + (false, Some(path)) => format!("file {}", path.display()), + (false, None) => "stdout off · no file sink".to_owned(), + }; + + EvaluatedCheck { + severity: DoctorSeverity::Ok, + view: DoctorCheckView { + name: "logging".to_owned(), + status: "ok".to_owned(), + detail, + }, + action: None, + } +} + +fn collect_actions(checks: &[EvaluatedCheck]) -> Vec<String> { + let mut actions = Vec::new(); + for action in checks.iter().filter_map(|check| check.action) { + if !actions.iter().any(|existing| existing == action) { + actions.push(action.to_owned()); + } + } + actions +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod doctor; pub mod identity; pub mod myc; pub mod runtime; @@ -37,7 +38,7 @@ pub fn dispatch( Command::Signer(signer) => match &signer.command { SignerCommand::Status => Ok(signer::status(config)), }, - Command::Doctor => unimplemented_command("doctor"), + Command::Doctor => Ok(doctor::report(config, logging)), Command::Find(_) => unimplemented_command("find"), Command::Job(job) => match &job.command { JobCommand::Ls => unimplemented_command("job ls"), diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs @@ -1,18 +1,23 @@ use crate::domain::runtime::{ - AccountRuntimeView, ConfigShowView, LoggingRuntimeView, MycRuntimeView, OutputRuntimeView, - PathsRuntimeView, SignerRuntimeView, + AccountRuntimeView, ConfigFilesRuntimeView, ConfigShowView, LoggingRuntimeView, MycRuntimeView, + OutputRuntimeView, PathsRuntimeView, SignerRuntimeView, }; use crate::runtime::config::RuntimeConfig; use crate::runtime::logging::LoggingState; pub fn show(config: &RuntimeConfig, logging: &LoggingState) -> ConfigShowView { ConfigShowView { + source: "local runtime state".to_owned(), output: OutputRuntimeView { format: config.output.format.as_str().to_owned(), verbosity: config.output.verbosity.as_str().to_owned(), color: config.output.color, dry_run: config.output.dry_run, }, + config_files: ConfigFilesRuntimeView { + user_present: config.paths.user_config_path.exists(), + workspace_present: config.paths.workspace_config_path.exists(), + }, paths: PathsRuntimeView { user_config_path: config.paths.user_config_path.display().to_string(), workspace_config_path: config.paths.workspace_config_path.display().to_string(), diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -70,13 +70,16 @@ pub enum CommandView { AccountNew(AccountNewView), AccountWhoami(AccountWhoamiView), ConfigShow(ConfigShowView), + Doctor(DoctorView), MycStatus(MycStatusView), SignerStatus(SignerStatusView), } #[derive(Debug, Clone, Serialize)] pub struct ConfigShowView { + pub source: String, pub output: OutputRuntimeView, + pub config_files: ConfigFilesRuntimeView, pub paths: PathsRuntimeView, pub logging: LoggingRuntimeView, pub account: AccountRuntimeView, @@ -93,6 +96,12 @@ pub struct OutputRuntimeView { } #[derive(Debug, Clone, Serialize)] +pub struct ConfigFilesRuntimeView { + pub user_present: bool, + pub workspace_present: bool, +} + +#[derive(Debug, Clone, Serialize)] pub struct LoggingRuntimeView { pub initialized: bool, pub filter: String, @@ -124,6 +133,23 @@ pub struct MycRuntimeView { } #[derive(Debug, Clone, Serialize)] +pub struct DoctorView { + pub ok: bool, + pub state: String, + pub checks: Vec<DoctorCheckView>, + pub source: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub actions: Vec<String>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DoctorCheckView { + pub name: String, + pub status: String, + pub detail: String, +} + +#[derive(Debug, Clone, Serialize)] pub struct IdentityPublicView { pub id: String, pub public_key_hex: String, diff --git a/src/render/mod.rs b/src/render/mod.rs @@ -1,9 +1,11 @@ use std::io::{self, Write}; -use crate::domain::runtime::{CommandOutput, CommandView}; +use crate::domain::runtime::{CommandOutput, CommandView, DoctorCheckView, DoctorView}; use crate::runtime::RuntimeError; use crate::runtime::config::{OutputConfig, OutputFormat}; +const THIN_RULE: &str = "────────────────────────────────────────────────────"; + pub fn render_output(output: &CommandOutput, config: &OutputConfig) -> Result<(), RuntimeError> { match config.format { OutputFormat::Human => render_human(output), @@ -60,40 +62,10 @@ fn render_human_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), render_myc_status(stdout, view)?; } CommandView::ConfigShow(view) => { - writeln!(stdout, "config")?; - writeln!(stdout, "output")?; - writeln!(stdout, " format: {}", view.output.format)?; - writeln!(stdout, " verbosity: {}", view.output.verbosity)?; - writeln!(stdout, " color: {}", yes_no(view.output.color))?; - writeln!(stdout, " dry run: {}", yes_no(view.output.dry_run))?; - writeln!(stdout, "paths")?; - writeln!(stdout, " user config: {}", view.paths.user_config_path)?; - writeln!( - stdout, - " workspace config: {}", - view.paths.workspace_config_path - )?; - writeln!(stdout, " user state root: {}", view.paths.user_state_root)?; - writeln!(stdout, "logging")?; - writeln!( - stdout, - " initialized: {}", - yes_no(view.logging.initialized) - )?; - writeln!(stdout, " filter: {}", view.logging.filter)?; - writeln!(stdout, " stdout: {}", yes_no(view.logging.stdout))?; - if let Some(directory) = &view.logging.directory { - writeln!(stdout, " directory: {directory}")?; - } - if let Some(current_file) = &view.logging.current_file { - writeln!(stdout, " current file: {current_file}")?; - } - writeln!(stdout, "account")?; - writeln!(stdout, " identity path: {}", view.account.identity_path)?; - writeln!(stdout, "signer")?; - writeln!(stdout, " backend: {}", view.signer.backend)?; - writeln!(stdout, "myc")?; - writeln!(stdout, " executable: {}", view.myc.executable)?; + render_config_show(stdout, view)?; + } + CommandView::Doctor(view) => { + render_doctor(stdout, view)?; } CommandView::SignerStatus(view) => { writeln!(stdout, "signer")?; @@ -136,6 +108,10 @@ fn render_json_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), serde_json::to_writer_pretty(&mut *stdout, view)?; writeln!(stdout)?; } + CommandView::Doctor(view) => { + serde_json::to_writer_pretty(&mut *stdout, view)?; + writeln!(stdout)?; + } CommandView::SignerStatus(view) => { serde_json::to_writer_pretty(&mut *stdout, view)?; writeln!(stdout)?; @@ -160,6 +136,126 @@ fn yes_no(value: bool) -> &'static str { if value { "yes" } else { "no" } } +fn present_absent(value: bool) -> &'static str { + if value { "present" } else { "absent" } +} + +fn render_config_show( + stdout: &mut dyn Write, + view: &crate::domain::runtime::ConfigShowView, +) -> Result<(), RuntimeError> { + write_context(stdout, "config · effective")?; + render_pairs( + stdout, + "output", + &[ + ("format", view.output.format.as_str()), + ("verbosity", view.output.verbosity.as_str()), + ("color", yes_no(view.output.color)), + ("dry run", yes_no(view.output.dry_run)), + ], + )?; + let user_config = format!( + "{} · {}", + present_absent(view.config_files.user_present), + view.paths.user_config_path + ); + let workspace_config = format!( + "{} · {}", + present_absent(view.config_files.workspace_present), + view.paths.workspace_config_path + ); + render_pairs( + stdout, + "config roots", + &[ + ("user config", user_config.as_str()), + ("workspace config", workspace_config.as_str()), + ("user state root", view.paths.user_state_root.as_str()), + ], + )?; + + let mut logging_rows = vec![ + ("filter", view.logging.filter.as_str()), + ("stdout", yes_no(view.logging.stdout)), + ]; + if let Some(directory) = &view.logging.directory { + logging_rows.push(("directory", directory.as_str())); + } + if let Some(current_file) = &view.logging.current_file { + logging_rows.push(("file", current_file.as_str())); + } + render_pairs(stdout, "logging", logging_rows.as_slice())?; + render_pairs( + stdout, + "account", + &[("identity path", view.account.identity_path.as_str())], + )?; + render_pairs( + stdout, + "signer", + &[("backend", view.signer.backend.as_str())], + )?; + render_pairs( + stdout, + "myc", + &[("executable", view.myc.executable.as_str())], + )?; + writeln!(stdout, "source: {}", view.source)?; + Ok(()) +} + +fn render_doctor(stdout: &mut dyn Write, view: &DoctorView) -> Result<(), RuntimeError> { + write_context(stdout, "system · checks")?; + let table = Table { + headers: &["check", "status", "detail"], + rows: view.checks.iter().map(doctor_row).collect(), + }; + render_table(stdout, &table)?; + if !view.actions.is_empty() { + writeln!(stdout)?; + writeln!(stdout, "actions")?; + for action in &view.actions { + writeln!(stdout, " › {action}")?; + } + } + writeln!(stdout)?; + writeln!(stdout, "source: {}", view.source)?; + Ok(()) +} + +fn doctor_row(check: &DoctorCheckView) -> Vec<String> { + vec![ + check.name.clone(), + check.status.clone(), + check.detail.clone(), + ] +} + +fn write_context(stdout: &mut dyn Write, line: &str) -> Result<(), RuntimeError> { + writeln!(stdout, "{line}")?; + writeln!(stdout, "{THIN_RULE}")?; + Ok(()) +} + +fn render_pairs( + stdout: &mut dyn Write, + heading: &str, + rows: &[(&str, &str)], +) -> Result<(), RuntimeError> { + writeln!(stdout, "{heading}")?; + let label_width = rows + .iter() + .map(|(label, _)| label.len()) + .max() + .unwrap_or_default(); + for (label, value) in rows { + writeln!(stdout, " {label:label_width$} {value}")?; + } + writeln!(stdout)?; + Ok(()) +} + fn render_local_signer( stdout: &mut dyn Write, heading: &str, @@ -280,6 +376,7 @@ fn human_command_name(view: &CommandView) -> &'static str { CommandView::AccountNew(_) => "account new", CommandView::AccountWhoami(_) => "account whoami", CommandView::ConfigShow(_) => "config show", + CommandView::Doctor(_) => "doctor", CommandView::MycStatus(_) => "myc status", CommandView::SignerStatus(_) => "signer status", } @@ -289,7 +386,9 @@ fn human_command_name(view: &CommandView) -> &'static str { mod tests { use super::{Table, render_human_to, render_ndjson_to, render_table}; use crate::commands::runtime; - use crate::domain::runtime::{CommandOutput, CommandView, MycStatusView}; + use crate::domain::runtime::{ + CommandOutput, CommandView, DoctorCheckView, DoctorView, MycStatusView, + }; use crate::runtime::config::{ IdentityConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity, @@ -404,6 +503,37 @@ mod tests { } #[test] + fn human_render_doctor_uses_check_table_and_actions() { + let output = CommandOutput::unconfigured(CommandView::Doctor(DoctorView { + ok: false, + state: "warn".to_owned(), + checks: vec![ + DoctorCheckView { + name: "config".to_owned(), + status: "ok".to_owned(), + detail: "defaults active".to_owned(), + }, + DoctorCheckView { + name: "account".to_owned(), + status: "warn".to_owned(), + detail: "no local account at identity.json".to_owned(), + }, + ], + source: "local diagnostics".to_owned(), + actions: vec!["radroots account new".to_owned()], + })); + let mut buffer = Vec::new(); + render_human_to(&mut buffer, &output).expect("render human"); + let rendered = String::from_utf8(buffer).expect("utf8"); + assert!(rendered.contains("system · checks")); + assert!(rendered.contains("check")); + assert!(rendered.contains("account warn")); + assert!(rendered.contains("actions")); + assert!(rendered.contains("› radroots account new")); + assert!(rendered.contains("source: local diagnostics")); + } + + #[test] fn table_renderer_aligns_columns() { let table = Table { headers: &["item", "status"], diff --git a/tests/doctor.rs b/tests/doctor.rs @@ -0,0 +1,111 @@ +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_LOG_FILTER", + "RADROOTS_LOG_DIR", + "RADROOTS_LOG_STDOUT", + "RADROOTS_IDENTITY_PATH", + "RADROOTS_SIGNER_BACKEND", + "RADROOTS_MYC_EXECUTABLE", + ] { + command.env_remove(key); + } + 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["checks"][2]["name"], "signer"); + assert_eq!(json["checks"][2]["status"], "warn"); + assert_eq!(json["source"], "local diagnostics"); + assert_eq!(json["actions"][0], "radroots account new"); +} + +#[test] +fn doctor_reports_ready_local_bootstrap_state() { + let dir = tempdir().expect("tempdir"); + let identity_path = dir.path().join("identity.json"); + let init = doctor_command_in(dir.path()) + .args([ + "--json", + "--identity-path", + identity_path.to_str().expect("identity path"), + "account", + "new", + ]) + .output() + .expect("run account new"); + assert!(init.status.success()); + + let output = doctor_command_in(dir.path()) + .args([ + "--json", + "--identity-path", + identity_path.to_str().expect("identity path"), + "doctor", + ]) + .output() + .expect("run doctor"); + + 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["ok"], true); + assert_eq!(json["state"], "ok"); + assert_eq!(json["checks"][1]["name"], "account"); + assert_eq!(json["checks"][1]["status"], "ok"); + assert_eq!(json["checks"][2]["name"], "signer"); + assert_eq!(json["checks"][2]["status"], "ok"); + assert_eq!(json["actions"], Value::Null); +} + +#[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"); + + 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"], "signer"); + assert_eq!(json["checks"][2]["status"], "fail"); + assert_eq!(json["checks"][3]["name"], "myc"); + assert_eq!(json["checks"][3]["status"], "fail"); + assert_eq!(json["source"], "local diagnostics + myc status command"); +} diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs @@ -40,6 +40,7 @@ fn config_show_json_reports_default_bootstrap_state() { 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); @@ -70,6 +71,8 @@ fn config_show_json_reports_default_bootstrap_state() { assert_eq!(json["logging"]["initialized"], true); assert_eq!(json["logging"]["stdout"], false); assert_eq!(json["logging"]["directory"], Value::Null); + assert_eq!(json["config_files"]["user_present"], false); + assert_eq!(json["config_files"]["workspace_present"], false); assert_eq!(json["account"]["identity_path"], "identity.json"); assert_eq!(json["signer"]["backend"], "local"); assert_eq!(json["myc"]["executable"], "myc");