cli

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

commit 70335dcc0cabd9e3a94fc16c42ae64958a3f0ea8
parent 0c42bc124066d5f728c5408bc51113dc9676f2f5
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Apr 2026 05:08:12 +0000

cli: add signer runtime test harness

- add shared cli sandbox integration helpers
- add fake myc executable fixture support
- prove local and myc signer status smoke paths
- reuse sandbox setup in target cli integration tests

Diffstat:
Atests/signer_runtime_modes.rs | 32++++++++++++++++++++++++++++++++
Atests/support/mod.rs | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/target_cli.rs | 53+++++++++++++++++------------------------------------
3 files changed, 147 insertions(+), 36 deletions(-)

diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs @@ -0,0 +1,32 @@ +mod support; + +use support::RadrootsCliSandbox; + +#[test] +fn harness_runs_local_signer_status_with_json_envelope() { + let sandbox = RadrootsCliSandbox::new(); + + let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); + + assert_eq!(value["schema_version"], "radroots.cli.output.v1"); + assert_eq!(value["operation_id"], "signer.status.get"); + assert_eq!(value["kind"], "signer.status.get"); + assert_eq!(value["result"]["mode"], "local"); +} + +#[cfg(unix)] +#[test] +fn harness_runs_myc_signer_status_with_fake_executable() { + let sandbox = RadrootsCliSandbox::new(); + let myc = sandbox.write_fake_myc("myc-invalid-json", "printf 'not json\\n'"); + sandbox.write_app_config(&format!( + "[signer]\nmode = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n", + myc.display() + )); + + let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); + + assert_eq!(value["operation_id"], "signer.status.get"); + assert_eq!(value["result"]["mode"], "myc"); + assert_eq!(value["result"]["myc"]["state"], "unavailable"); +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs @@ -0,0 +1,98 @@ +#![allow(dead_code)] + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use assert_cmd::prelude::*; +use serde_json::Value; +use tempfile::TempDir; + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + +pub fn radroots() -> Command { + Command::cargo_bin("radroots").expect("binary") +} + +pub fn json_from_stdout(output: &Output) -> Value { + serde_json::from_slice(&output.stdout).unwrap_or_else(|error| { + panic!( + "stdout was not json: {error}; stderr `{}`; stdout `{}`", + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + ) + }) +} + +pub struct RadrootsCliSandbox { + root: TempDir, +} + +impl RadrootsCliSandbox { + pub fn new() -> Self { + Self { + root: TempDir::new().expect("tempdir"), + } + } + + pub fn root(&self) -> &Path { + self.root.path() + } + + pub fn command(&self) -> Command { + let mut command = radroots(); + self.apply_base_env(&mut command); + command + } + + pub fn json_success(&self, args: &[&str]) -> Value { + let output = self.command().args(args).output().expect("run command"); + assert!( + output.status.success(), + "`{args:?}` failed with stderr `{}` and stdout `{}`", + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + ); + json_from_stdout(&output) + } + + pub fn json_output(&self, args: &[&str]) -> (Output, Value) { + let output = self.command().args(args).output().expect("run command"); + let value = json_from_stdout(&output); + (output, value) + } + + pub fn write_workspace_config(&self, raw: &str) -> PathBuf { + let path = self.root.path().join("config.toml"); + fs::write(&path, raw).expect("write workspace config"); + path + } + + pub fn write_app_config(&self, raw: &str) -> PathBuf { + let path = self.root.path().join("config/apps/cli/config.toml"); + fs::create_dir_all(path.parent().expect("app config parent")).expect("app config dir"); + fs::write(&path, raw).expect("write app config"); + path + } + + #[cfg(unix)] + pub fn write_fake_myc(&self, name: &str, body: &str) -> PathBuf { + let path = self.root.path().join("bin").join(name); + fs::create_dir_all(path.parent().expect("fake myc parent")).expect("fake myc dir"); + fs::write(&path, format!("#!/bin/sh\nset -eu\n{body}\n")).expect("write fake myc"); + let mut permissions = fs::metadata(&path) + .expect("fake myc metadata") + .permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&path, permissions).expect("fake myc executable"); + path + } + + fn apply_base_env(&self, command: &mut Command) { + command.env("RADROOTS_CLI_PATHS_PROFILE", "repo_local"); + command.env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", self.root.path()); + command.env("RADROOTS_ACCOUNT_SECRET_BACKEND", "encrypted_file"); + command.env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "none"); + } +} diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -1,33 +1,14 @@ -use std::process::Command; +mod support; -use assert_cmd::prelude::*; use serde_json::Value; -use tempfile::TempDir; + +use support::{RadrootsCliSandbox, radroots}; const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg"; -fn radroots() -> Command { - Command::cargo_bin("radroots").expect("binary") -} - -fn radroots_in(root: &TempDir) -> Command { - let mut command = radroots(); - command.env("RADROOTS_CLI_PATHS_PROFILE", "repo_local"); - command.env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", root.path()); - command -} - -fn json_success(root: &TempDir, args: &[&str]) -> Value { - let output = radroots_in(root).args(args).output().expect("run command"); - - assert!( - output.status.success(), - "`{args:?}` failed with stderr `{}` and stdout `{}`", - String::from_utf8_lossy(&output.stderr), - String::from_utf8_lossy(&output.stdout) - ); - serde_json::from_slice(&output.stdout).expect("json envelope") +fn json_success(sandbox: &RadrootsCliSandbox, args: &[&str]) -> Value { + sandbox.json_success(args) } #[test] @@ -162,24 +143,24 @@ fn required_approval_missing_token_returns_structured_error() { #[test] fn buyer_mvp_flow_acceptance_uses_target_operations() { - let root = TempDir::new().expect("tempdir"); + let sandbox = RadrootsCliSandbox::new(); let search = json_success( - &root, + &sandbox, &["--format", "json", "market", "product", "search", "eggs"], ); assert_eq!(search["operation_id"], "market.product.search"); assert_eq!(search["errors"].as_array().expect("errors").len(), 0); let create = json_success( - &root, + &sandbox, &["--format", "json", "basket", "create", "basket_flow"], ); assert_eq!(create["operation_id"], "basket.create"); assert_eq!(create["result"]["basket_id"], "basket_flow"); let add = json_success( - &root, + &sandbox, &[ "--format", "json", @@ -199,7 +180,7 @@ fn buyer_mvp_flow_acceptance_uses_target_operations() { assert_eq!(add["result"]["ready_for_quote"], true); let quote = json_success( - &root, + &sandbox, &[ "--format", "json", @@ -216,7 +197,7 @@ fn buyer_mvp_flow_acceptance_uses_target_operations() { .expect("order id"); let submit = json_success( - &root, + &sandbox, &["--format", "json", "--dry-run", "order", "submit", order_id], ); assert_eq!(submit["operation_id"], "order.submit"); @@ -226,12 +207,12 @@ fn buyer_mvp_flow_acceptance_uses_target_operations() { #[test] fn seller_mvp_flow_acceptance_uses_target_operations() { - let root = TempDir::new().expect("tempdir"); - let listing_file = root.path().join("listing.toml"); + let sandbox = RadrootsCliSandbox::new(); + let listing_file = sandbox.root().join("listing.toml"); let listing_file = listing_file.to_string_lossy().into_owned(); let create = json_success( - &root, + &sandbox, &[ "--format", "json", @@ -265,7 +246,7 @@ fn seller_mvp_flow_acceptance_uses_target_operations() { assert_eq!(create["result"]["file"], listing_file); let validate = json_success( - &root, + &sandbox, &[ "--format", "json", @@ -278,7 +259,7 @@ fn seller_mvp_flow_acceptance_uses_target_operations() { assert!(validate["result"]["valid"].is_boolean()); let publish = json_success( - &root, + &sandbox, &[ "--format", "json", @@ -291,7 +272,7 @@ fn seller_mvp_flow_acceptance_uses_target_operations() { assert_eq!(publish["operation_id"], "listing.publish"); assert_eq!(publish["result"]["state"], "dry_run"); - let orders = json_success(&root, &["--format", "json", "order", "list"]); + let orders = json_success(&sandbox, &["--format", "json", "order", "list"]); assert_eq!(orders["operation_id"], "order.list"); assert_eq!(orders["errors"].as_array().expect("errors").len(), 0); }