commit a276c378cfd5c033d300da0350b580064e341d87
parent edc766dcb8f4607d7fd51c467e6099c6251ef88b
Author: triesap <tyson@radroots.org>
Date: Sat, 25 Apr 2026 10:08:54 +0000
signer: consume myc signer status contract
Diffstat:
24 files changed, 325 insertions(+), 20 deletions(-)
diff --git a/src/cli.rs b/src/cli.rs
@@ -71,6 +71,7 @@ Global options
--account <ACCOUNT>
--signer <SIGNER>
--relay <RELAY>
+ --myc-status-timeout-ms <MS>
Examples
radroots setup seller
@@ -205,6 +206,8 @@ pub struct CliArgs {
pub relay: Vec<String>,
#[arg(long, global = true)]
pub myc_executable: Option<PathBuf>,
+ #[arg(long = "myc-status-timeout-ms", global = true)]
+ pub myc_status_timeout_ms: Option<u64>,
#[arg(long = "hyf-enabled", global = true, action = ArgAction::SetTrue)]
pub hyf_enabled: bool,
#[arg(long = "no-hyf-enabled", global = true, action = ArgAction::SetTrue)]
@@ -1315,6 +1318,8 @@ mod tests {
"wss://relay.two",
"--myc-executable",
"bin/myc",
+ "--myc-status-timeout-ms",
+ "2500",
"--hyf-enabled",
"--hyf-executable",
"bin/hyfd",
@@ -1361,6 +1366,7 @@ mod tests {
.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
diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs
@@ -135,6 +135,7 @@ pub fn show(
},
myc: MycRuntimeView {
executable: config.myc.executable.display().to_string(),
+ status_timeout_ms: config.myc.status_timeout_ms,
},
write_plane: WritePlaneRuntimeView {
provider_runtime_id: write_plane.provider_runtime_id,
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -412,6 +412,7 @@ pub struct LocalRuntimeView {
#[derive(Debug, Clone, Serialize)]
pub struct MycRuntimeView {
pub executable: String,
+ pub status_timeout_ms: u64,
}
#[derive(Debug, Clone, Serialize)]
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -1259,10 +1259,14 @@ fn render_config_show(
("exports dir", view.local.exports_dir.as_str()),
],
)?;
+ let myc_status_timeout_ms = view.myc.status_timeout_ms.to_string();
render_pairs(
stdout,
"myc",
- &[("executable", view.myc.executable.as_str())],
+ &[
+ ("executable", view.myc.executable.as_str()),
+ ("status timeout ms", myc_status_timeout_ms.as_str()),
+ ],
)?;
let write_plane_target = format_runtime_target(
view.write_plane.target_kind.as_deref(),
@@ -4516,6 +4520,7 @@ mod tests {
},
myc: MycConfig {
executable: "myc".into(),
+ status_timeout_ms: 2_000,
},
hyf: HyfConfig {
enabled: false,
@@ -4671,6 +4676,7 @@ mod tests {
},
myc: MycConfig {
executable: "myc".into(),
+ status_timeout_ms: 2_000,
},
hyf: HyfConfig {
enabled: false,
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -24,6 +24,7 @@ const DEFAULT_LOCAL_DB_FILE: &str = "replica.sqlite";
const DEFAULT_LOCAL_BACKUPS_DIR: &str = "backups";
const DEFAULT_LOCAL_EXPORTS_DIR: &str = "exports";
const DEFAULT_SHARED_ACCOUNTS_STORE_FILE: &str = "store.json";
+const DEFAULT_MYC_STATUS_TIMEOUT_MS: u64 = 2_000;
const DEFAULT_HYF_EXECUTABLE: &str = "hyfd";
const DEFAULT_RPC_URL: &str = "http://127.0.0.1:7070";
const CLI_HOST_VAULT_POLICY: &str = "desktop";
@@ -46,6 +47,7 @@ const ENV_IDENTITY_PATH: &str = "RADROOTS_IDENTITY_PATH";
const ENV_SIGNER: &str = "RADROOTS_SIGNER";
const ENV_RELAYS: &str = "RADROOTS_RELAYS";
const ENV_MYC_EXECUTABLE: &str = "RADROOTS_MYC_EXECUTABLE";
+const ENV_MYC_STATUS_TIMEOUT_MS: &str = "RADROOTS_MYC_STATUS_TIMEOUT_MS";
const ENV_HYF_ENABLED: &str = "RADROOTS_HYF_ENABLED";
const ENV_HYF_EXECUTABLE: &str = "RADROOTS_HYF_EXECUTABLE";
const ENV_RPC_URL: &str = "RADROOTS_RPC_URL";
@@ -67,6 +69,7 @@ const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_SIGNER,
ENV_RELAYS,
ENV_MYC_EXECUTABLE,
+ ENV_MYC_STATUS_TIMEOUT_MS,
ENV_HYF_ENABLED,
ENV_HYF_EXECUTABLE,
ENV_RPC_URL,
@@ -229,6 +232,7 @@ pub struct LocalConfig {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MycConfig {
pub executable: PathBuf,
+ pub status_timeout_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -351,6 +355,7 @@ impl EnvFileValues {
#[derive(Debug, Default, Deserialize)]
struct CliConfigFile {
relay: Option<RelayFileConfig>,
+ myc: Option<MycFileConfig>,
hyf: Option<HyfFileConfig>,
rpc: Option<RpcFileConfig>,
capability_binding: Option<Vec<CapabilityBindingFileConfig>>,
@@ -368,6 +373,12 @@ struct RpcFileConfig {
}
#[derive(Debug, Default, Deserialize)]
+struct MycFileConfig {
+ executable: Option<PathBuf>,
+ status_timeout_ms: Option<u64>,
+}
+
+#[derive(Debug, Default, Deserialize)]
struct HyfFileConfig {
enabled: Option<bool>,
executable: Option<PathBuf>,
@@ -582,13 +593,13 @@ impl RuntimeConfig {
.join(DEFAULT_LOCAL_STATE_DIR)
.join(DEFAULT_LOCAL_EXPORTS_DIR),
},
- myc: MycConfig {
- executable: args
- .myc_executable
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_MYC_EXECUTABLE]).map(PathBuf::from))
- .unwrap_or_else(|| PathBuf::from("myc")),
- },
+ myc: resolve_myc_config(
+ args,
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?,
hyf: HyfConfig {
enabled: resolve_hyf_enabled(
args,
@@ -941,6 +952,85 @@ fn resolve_relay_config(
})
}
+fn resolve_myc_config(
+ args: &CliArgs,
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+) -> Result<MycConfig, RuntimeError> {
+ let executable = args
+ .myc_executable
+ .clone()
+ .or_else(|| env_value(env, env_file, &[ENV_MYC_EXECUTABLE]).map(PathBuf::from))
+ .or_else(|| {
+ user_config
+ .and_then(|config| config.myc.as_ref())
+ .and_then(|myc| myc.executable.clone())
+ })
+ .or_else(|| {
+ workspace_config
+ .and_then(|config| config.myc.as_ref())
+ .and_then(|myc| myc.executable.clone())
+ })
+ .unwrap_or_else(|| PathBuf::from("myc"));
+
+ Ok(MycConfig {
+ executable,
+ status_timeout_ms: resolve_myc_status_timeout_ms(
+ args,
+ env,
+ env_file,
+ user_config,
+ workspace_config,
+ )?,
+ })
+}
+
+fn resolve_myc_status_timeout_ms(
+ args: &CliArgs,
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+) -> Result<u64, RuntimeError> {
+ if let Some(value) = args.myc_status_timeout_ms {
+ return validate_myc_status_timeout_ms("--myc-status-timeout-ms", value);
+ }
+
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_MYC_STATUS_TIMEOUT_MS]) {
+ let parsed = value.trim().parse::<u64>().map_err(|err| {
+ RuntimeError::Config(format!("{key} must be an integer millisecond value: {err}"))
+ })?;
+ return validate_myc_status_timeout_ms(key.as_str(), parsed);
+ }
+
+ if let Some(value) = user_config
+ .and_then(|config| config.myc.as_ref())
+ .and_then(|myc| myc.status_timeout_ms)
+ {
+ return validate_myc_status_timeout_ms("user config [myc].status_timeout_ms", value);
+ }
+
+ if let Some(value) = workspace_config
+ .and_then(|config| config.myc.as_ref())
+ .and_then(|myc| myc.status_timeout_ms)
+ {
+ return validate_myc_status_timeout_ms("workspace config [myc].status_timeout_ms", value);
+ }
+
+ Ok(DEFAULT_MYC_STATUS_TIMEOUT_MS)
+}
+
+fn validate_myc_status_timeout_ms(source: &str, value: u64) -> Result<u64, RuntimeError> {
+ if value == 0 {
+ return Err(RuntimeError::Config(format!(
+ "{source} must be greater than zero"
+ )));
+ }
+ Ok(value)
+}
+
fn resolve_hyf_enabled(
args: &CliArgs,
env: &dyn Environment,
@@ -1467,6 +1557,8 @@ mod tests {
"wss://relay.two",
"--myc-executable",
"bin/myc-cli",
+ "--myc-status-timeout-ms",
+ "2500",
"--hyf-enabled",
"--hyf-executable",
"bin/hyfd-cli",
@@ -1484,6 +1576,10 @@ mod tests {
("RADROOTS_SIGNER".to_owned(), "myc".to_owned()),
("RADROOTS_RELAYS".to_owned(), "wss://relay.env".to_owned()),
("RADROOTS_MYC_EXECUTABLE".to_owned(), "env-myc".to_owned()),
+ (
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "9000".to_owned(),
+ ),
("RADROOTS_HYF_ENABLED".to_owned(), "false".to_owned()),
("RADROOTS_HYF_EXECUTABLE".to_owned(), "env-hyfd".to_owned()),
]));
@@ -1576,6 +1672,7 @@ mod tests {
assert_eq!(resolved.relay.source, RelayConfigSource::Flags);
assert_eq!(resolved.relay.publish_policy, RelayPublishPolicy::Any);
assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc-cli"));
+ assert_eq!(resolved.myc.status_timeout_ms, 2500);
assert_eq!(
resolved.hyf,
HyfConfig {
@@ -1607,6 +1704,10 @@ mod tests {
"wss://relay.one,wss://relay.two".to_owned(),
),
("RADROOTS_MYC_EXECUTABLE".to_owned(), "bin/myc".to_owned()),
+ (
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "3500".to_owned(),
+ ),
("RADROOTS_HYF_ENABLED".to_owned(), "true".to_owned()),
("RADROOTS_HYF_EXECUTABLE".to_owned(), "bin/hyfd".to_owned()),
]));
@@ -1656,6 +1757,7 @@ mod tests {
);
assert_eq!(resolved.relay.source, RelayConfigSource::Environment);
assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc"));
+ assert_eq!(resolved.myc.status_timeout_ms, 3500);
assert_eq!(
resolved.hyf,
HyfConfig {
@@ -1804,6 +1906,21 @@ mod tests {
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect_err("invalid bool");
assert!(error.to_string().contains("RADROOTS_LOG_STDOUT"));
+
+ let env = MapEnvironment::new(BTreeMap::from([(
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "slow".to_owned(),
+ )]));
+ let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
+ .expect_err("invalid myc timeout");
+ assert!(error.to_string().contains("RADROOTS_MYC_STATUS_TIMEOUT_MS"));
+
+ let args =
+ CliArgs::parse_from(["radroots", "--myc-status-timeout-ms", "0", "config", "show"]);
+ let env = MapEnvironment::new(BTreeMap::new());
+ let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
+ .expect_err("zero myc timeout");
+ assert!(error.to_string().contains("greater than zero"));
}
#[test]
@@ -1821,6 +1938,7 @@ RADROOTS_IDENTITY_PATH=state/identity.json
RADROOTS_SIGNER=myc
RADROOTS_RELAYS=wss://relay.env-file
RADROOTS_MYC_EXECUTABLE=bin/myc
+RADROOTS_MYC_STATUS_TIMEOUT_MS=4500
RADROOTS_HYF_ENABLED=true
RADROOTS_HYF_EXECUTABLE=bin/hyfd
"#,
@@ -1843,6 +1961,7 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd
assert_eq!(resolved.relay.urls, vec!["wss://relay.env-file".to_owned()]);
assert_eq!(resolved.relay.source, RelayConfigSource::Environment);
assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc"));
+ assert_eq!(resolved.myc.status_timeout_ms, 4500);
assert_eq!(
resolved.hyf,
HyfConfig {
@@ -2040,6 +2159,56 @@ RADROOTS_CLI_LOGGING_STDOUT=false
}
#[test]
+ fn user_myc_config_overrides_workspace_myc_config() {
+ let temp = tempdir().expect("tempdir");
+ let workspace_root = temp.path().join("workspace");
+ let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
+ let app_config_dir = repo_local_root.join("config/apps/cli");
+ let user_home = temp.path().join("home");
+ fs::create_dir_all(&repo_local_root).expect("workspace config dir");
+ fs::create_dir_all(&app_config_dir).expect("app config dir");
+ fs::write(
+ repo_local_root.join("config.toml"),
+ "[myc]\nexecutable = \"workspace-myc\"\nstatus_timeout_ms = 9000\n",
+ )
+ .expect("write workspace config");
+ fs::write(
+ app_config_dir.join("config.toml"),
+ "[myc]\nexecutable = \"user-myc\"\nstatus_timeout_ms = 3000\n",
+ )
+ .expect("write user config");
+
+ let env = MapEnvironment {
+ values: BTreeMap::from([
+ (
+ "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
+ "repo_local".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
+ repo_local_root.display().to_string(),
+ ),
+ ]),
+ current_dir: workspace_root,
+ path_resolver: RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(user_home),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
+ stdin_tty: false,
+ stdout_tty: false,
+ };
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+
+ let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
+ .expect("resolve config");
+ assert_eq!(resolved.myc.executable, PathBuf::from("user-myc"));
+ assert_eq!(resolved.myc.status_timeout_ms, 3000);
+ }
+
+ #[test]
fn user_capability_binding_overrides_workspace_binding() {
let temp = tempdir().expect("tempdir");
let workspace_root = temp.path().join("workspace");
diff --git a/src/runtime/myc.rs b/src/runtime/myc.rs
@@ -14,7 +14,8 @@ use crate::domain::runtime::{
};
use crate::runtime::config::MycConfig;
-const MYC_STATUS_TIMEOUT: Duration = Duration::from_secs(1);
+const MYC_SIGNER_STATUS_CONTRACT_VERSION: u32 = 1;
+const MYC_STATUS_VIEW: &str = "signer";
const MYC_STATUS_POLL_INTERVAL: Duration = Duration::from_millis(10);
pub fn resolve_status(config: &MycConfig) -> MycStatusView {
@@ -55,7 +56,7 @@ pub fn resolve_status(config: &MycConfig) -> MycStatusView {
"unavailable",
format!(
"myc status command timed out after {}ms",
- MYC_STATUS_TIMEOUT.as_millis()
+ config.status_timeout_ms
),
);
}
@@ -95,24 +96,46 @@ pub fn resolve_status(config: &MycConfig) -> MycStatusView {
}
};
- let payload = match serde_json::from_str::<MycStatusPayload>(stdout.as_str()) {
+ let payload_value = match serde_json::from_str::<serde_json::Value>(stdout.as_str()) {
Ok(payload) => payload,
Err(error) => {
return unavailable_status(
executable,
"unavailable",
- format!("myc status output was not valid JSON: {error}"),
+ format!("myc signer status output was not valid JSON: {error}"),
+ );
+ }
+ };
+ let payload = match serde_json::from_value::<MycStatusPayload>(payload_value) {
+ Ok(payload) => payload,
+ Err(error) => {
+ return unavailable_status(
+ executable,
+ "unavailable",
+ format!(
+ "myc signer status output did not match contract version {MYC_SIGNER_STATUS_CONTRACT_VERSION}: {error}"
+ ),
);
}
};
let MycStatusPayload {
+ status_contract_version,
status,
ready,
reasons,
signer_backend,
custody,
} = payload;
+ if status_contract_version != MYC_SIGNER_STATUS_CONTRACT_VERSION {
+ return unavailable_status(
+ executable,
+ "unavailable",
+ format!(
+ "myc signer status contract version {status_contract_version} is incompatible with cli expected {MYC_SIGNER_STATUS_CONTRACT_VERSION}"
+ ),
+ );
+ }
let MycSignerBackendPayload {
local_signer,
remote_session_count,
@@ -153,7 +176,7 @@ pub fn resolve_status(config: &MycConfig) -> MycStatusView {
fn run_status_command(config: &MycConfig) -> Result<Output, MycCommandError> {
let mut child = Command::new(&config.executable)
- .args(["status", "--view", "full"])
+ .args(["status", "--view", MYC_STATUS_VIEW])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
@@ -163,11 +186,12 @@ fn run_status_command(config: &MycConfig) -> Result<Output, MycCommandError> {
})?;
let started_at = Instant::now();
+ let status_timeout = Duration::from_millis(config.status_timeout_ms);
loop {
match child.try_wait() {
Ok(Some(status)) => return collect_output(child, status),
Ok(None) => {
- if started_at.elapsed() >= MYC_STATUS_TIMEOUT {
+ if started_at.elapsed() >= status_timeout {
let _ = child.kill();
let _ = child.wait();
return Err(MycCommandError::Timeout);
@@ -277,6 +301,7 @@ enum MycCommandError {
#[derive(Debug, Deserialize)]
struct MycStatusPayload {
+ status_contract_version: u32,
status: String,
ready: bool,
#[serde(default)]
@@ -382,6 +407,7 @@ mod tests {
fn empty_executable_path_reports_unconfigured_status() {
let view = resolve_status(&MycConfig {
executable: PathBuf::new(),
+ status_timeout_ms: 2_000,
});
assert_eq!(view.state, "unconfigured");
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -816,6 +816,7 @@ mod tests {
},
myc: MycConfig {
executable: PathBuf::from("myc"),
+ status_timeout_ms: 2_000,
},
hyf: HyfConfig {
enabled: hyf_enabled,
diff --git a/tests/doctor.rs b/tests/doctor.rs
@@ -29,6 +29,7 @@ fn doctor_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/farm.rs b/tests/farm.rs
@@ -30,6 +30,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/find.rs b/tests/find.rs
@@ -44,6 +44,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs
@@ -48,6 +48,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/job_rpc.rs b/tests/job_rpc.rs
@@ -35,6 +35,7 @@ fn job_rpc_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/listing.rs b/tests/listing.rs
@@ -51,6 +51,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
@@ -97,7 +98,7 @@ fn write_fake_myc(dir: &Path, script: &str) -> std::path::PathBuf {
fn successful_status_script(payload_json: String) -> String {
format!(
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"full\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\ncat <<'JSON'\n{payload_json}\nJSON\n"
+ "#!/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"
)
}
@@ -114,6 +115,7 @@ fn sample_myc_status_payload(
.to_owned();
assert_ne!(signer_account_id, account_id);
json!({
+ "status_contract_version": 1,
"status": "healthy",
"ready": true,
"reasons": [],
diff --git a/tests/local.rs b/tests/local.rs
@@ -39,6 +39,7 @@ fn local_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/market.rs b/tests/market.rs
@@ -45,6 +45,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/myc_status.rs b/tests/myc_status.rs
@@ -33,6 +33,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
@@ -49,7 +50,7 @@ fn write_user_config(workdir: &Path, contents: &str) {
}
#[test]
-fn myc_status_reports_ready_for_valid_full_status_payload() {
+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(
@@ -115,6 +116,75 @@ fn myc_status_reports_unavailable_for_invalid_status_payload() {
}
#[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");
@@ -684,12 +754,14 @@ fn myc_status_reports_unavailable_for_timeout() {
let dir = tempdir().expect("tempdir");
let executable = write_fake_myc(
dir.path(),
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"full\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\nexec sleep 5\n",
+ "#!/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",
@@ -705,7 +777,7 @@ fn myc_status_reports_unavailable_for_timeout() {
assert!(
json["reason"]
.as_str()
- .is_some_and(|value| value.contains("timed out"))
+ .is_some_and(|value| value.contains("timed out after 25ms"))
);
}
@@ -727,7 +799,7 @@ fn myc_test_guard() -> MutexGuard<'static, ()> {
fn successful_status_script(payload_json: String) -> String {
format!(
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"full\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\ncat <<'JSON'\n{payload_json}\nJSON\n"
+ "#!/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"
)
}
@@ -751,6 +823,7 @@ fn sample_status_payload_with_permissions(ready: bool, permissions: &[&str]) ->
};
json!({
+ "status_contract_version": 1,
"status": service_status,
"ready": ready,
"reasons": reasons,
@@ -801,6 +874,7 @@ fn sample_multi_session_status_payload() -> Value {
let first_user_public_key_hex = first_user.public_key_hex.clone();
json!({
+ "status_contract_version": 1,
"status": "healthy",
"ready": true,
"reasons": [],
diff --git a/tests/order.rs b/tests/order.rs
@@ -55,6 +55,7 @@ fn order_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
@@ -185,7 +186,7 @@ fn write_fake_myc(dir: &Path, script: &str) -> std::path::PathBuf {
fn successful_status_script(payload_json: String) -> String {
format!(
- "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"full\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\ncat <<'JSON'\n{payload_json}\nJSON\n"
+ "#!/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"
)
}
@@ -202,6 +203,7 @@ fn sample_myc_status_payload(
.to_owned();
assert_ne!(signer_account_id, account_id);
json!({
+ "status_contract_version": 1,
"status": "healthy",
"ready": true,
"reasons": [],
diff --git a/tests/relay_net.rs b/tests/relay_net.rs
@@ -29,6 +29,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/runtime_management.rs b/tests/runtime_management.rs
@@ -80,6 +80,7 @@ fn runtime_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -83,6 +83,7 @@ fn runtime_show_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
@@ -286,6 +287,7 @@ fn config_show_json_reports_default_bootstrap_state() {
.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"],
@@ -541,6 +543,7 @@ fn config_show_json_reflects_environment_configuration() {
.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"])
@@ -564,6 +567,7 @@ fn config_show_json_reflects_environment_configuration() {
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);
}
diff --git a/tests/sell.rs b/tests/sell.rs
@@ -34,6 +34,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -39,6 +39,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/sync.rs b/tests/sync.rs
@@ -29,6 +29,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {
diff --git a/tests/workflow.rs b/tests/workflow.rs
@@ -39,6 +39,7 @@ fn cli_command_in(workdir: &Path) -> Command {
"RADROOTS_SIGNER",
"RADROOTS_RELAYS",
"RADROOTS_MYC_EXECUTABLE",
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS",
"RADROOTS_RPC_URL",
"RADROOTS_RPC_BEARER_TOKEN",
] {