#![allow(missing_docs)]
use assert_cmd::Command;
use std::process::Command as StdCommand;
fn encode(template: &str, extra_args: &[&str]) -> String {
let mut args = vec!["encode", "--group-size", "0"];
args.extend_from_slice(extra_args);
args.push(template);
let out = StdCommand::new(assert_cmd::cargo::cargo_bin("md"))
.args(&args)
.output()
.expect("invoke md encode");
assert!(
out.status.success(),
"encode {template:?} {extra_args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout)
.unwrap()
.lines()
.next()
.unwrap()
.to_string()
}
fn encode_chunked(template: &str, extra_args: &[&str]) -> Vec<String> {
let mut args = vec!["encode", "--force-chunked", "--group-size", "0"];
args.extend_from_slice(extra_args);
args.push(template);
let out = StdCommand::new(assert_cmd::cargo::cargo_bin("md"))
.args(&args)
.output()
.expect("invoke md encode --force-chunked");
assert!(
out.status.success(),
"encode --force-chunked {template:?} {extra_args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout)
.unwrap()
.lines()
.filter(|l| l.starts_with("md1"))
.map(String::from)
.collect()
}
const ORIGIN_MARKER: &str = "origin: \u{ab}unspecified \u{2014} supply on restore\u{bb}";
const DEAD_SHAPES: &[(&str, &str)] = &[
("tr_with_taptree", "tr(@0/<0;1>/*,pk(@1/<0;1>/*))"),
(
"sh_sortedmulti_legacy",
"sh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*))",
),
("bare_wsh_single_key", "wsh(pk(@0/<0;1>/*))"),
(
"raw_miniscript_body",
"wsh(or_d(pk(@0/<0;1>/*),and_v(v:pk(@1/<0;1>/*),older(144))))",
),
];
#[test]
fn decode_text_partial_renders_and_exits_4_for_every_dead_shape() {
for (name, template) in DEAD_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["decode", &phrase])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(4),
"[{name}] dead shape {template:?} must partial-decode with exit 4"
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains(*template),
"[{name}] template line missing; got {stdout:?}"
);
assert!(
stdout.contains(ORIGIN_MARKER),
"[{name}] origin-unspecified marker missing; got {stdout:?}"
);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
!stderr.is_empty(),
"[{name}] expected a stderr note on partial decode"
);
}
}
#[cfg(feature = "json")]
#[test]
fn decode_json_partial_has_reason_and_nonempty_indices_for_every_dead_shape() {
for (name, template) in DEAD_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["decode", &phrase, "--json"])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(4),
"[{name}] json partial-decode must exit 4"
);
let stdout = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
v["partial"]["reason"], "missing_explicit_origin",
"[{name}] partial.reason mismatch; got {v}"
);
let idxs = v["partial"]["unresolved_indices"]
.as_array()
.unwrap_or_else(|| panic!("[{name}] unresolved_indices must be an array; got {v}"));
assert!(
!idxs.is_empty(),
"[{name}] unresolved_indices must be non-empty; got {v}"
);
assert_eq!(
v["descriptor"]["path_decl"]["data"], "m",
"[{name}] raw path_decl must stay elided \"m\"; got {v}"
);
assert_eq!(v["schema"], "md-cli/1");
}
}
#[test]
fn inspect_text_partial_omits_policy_id_lines_for_every_dead_shape() {
for (name, template) in DEAD_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["inspect", &phrase])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(4),
"[{name}] inspect on dead shape must exit 4"
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains(&format!("template: {template}")),
"[{name}] template line missing; got {stdout:?}"
);
assert!(
stdout.contains(ORIGIN_MARKER),
"[{name}] origin marker missing; got {stdout:?}"
);
assert!(
stdout.contains("md1-encoding-id:"),
"[{name}] md1-encoding-id must stay present (no origin dep); got {stdout:?}"
);
assert!(
stdout.contains("wallet-descriptor-template-id:"),
"[{name}] wallet-descriptor-template-id must stay present; got {stdout:?}"
);
assert!(
!stdout.contains("wallet-policy-id:"),
"[{name}] wallet-policy-id line must be OMITTED under partial; got {stdout:?}"
);
assert!(
!stdout.contains("wallet-policy-id-fingerprint:"),
"[{name}] wallet-policy-id-fingerprint line must be OMITTED under partial; got {stdout:?}"
);
}
}
#[cfg(feature = "json")]
#[test]
fn inspect_json_partial_omits_wallet_policy_id_key_for_every_dead_shape() {
for (name, template) in DEAD_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["inspect", &phrase, "--json"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(4), "[{name}] must exit 4");
let stdout = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(
v.get("wallet_policy_id").is_none(),
"[{name}] wallet_policy_id key must be OMITTED under partial; got {v}"
);
assert!(
v.get("md1_encoding_id").is_some(),
"[{name}] md1_encoding_id must stay present; got {v}"
);
assert!(
v.get("wallet_descriptor_template_id").is_some(),
"[{name}] wallet_descriptor_template_id must stay present; got {v}"
);
assert_eq!(
v["partial"]["reason"], "missing_explicit_origin",
"[{name}] partial.reason mismatch; got {v}"
);
assert!(
!v["partial"]["unresolved_indices"]
.as_array()
.unwrap()
.is_empty(),
"[{name}] unresolved_indices must be non-empty; got {v}"
);
}
}
const CANONICAL_SHAPES: &[(&str, &str)] = &[
("tr_keypath_only", "tr(@0/<0;1>/*)"),
("wpkh_single_key", "wpkh(@0/<0;1>/*)"),
("sh_wpkh_nested", "sh(wpkh(@0/<0;1>/*))"),
("wsh_multi", "wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*))"),
];
#[test]
fn decode_text_canonical_shapes_stay_exit_0_and_never_show_origin_marker() {
for (name, template) in CANONICAL_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["decode", &phrase])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(0),
"[{name}] canonical shape {template:?} must stay exit 0"
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert_eq!(
stdout.trim_end(),
*template,
"[{name}] canonical decode text must be BYTE-IDENTICAL to the template line alone"
);
assert!(
!stdout.contains(ORIGIN_MARKER),
"[{name}] canonical shape must NEVER show the origin-unspecified marker"
);
}
}
#[cfg(feature = "json")]
#[test]
fn decode_json_canonical_shapes_never_carry_partial_key() {
for (name, template) in CANONICAL_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["decode", &phrase, "--json"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "[{name}] must stay exit 0");
let stdout = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(
v.get("partial").is_none(),
"[{name}] canonical shape JSON must NOT carry a `partial` key; got {v}"
);
}
}
#[test]
fn inspect_text_canonical_shapes_keep_all_policy_id_lines_and_exit_0() {
for (name, template) in CANONICAL_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["inspect", &phrase])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "[{name}] must stay exit 0");
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains("wallet-policy-id:"),
"[{name}] canonical shape must KEEP wallet-policy-id; got {stdout:?}"
);
assert!(
stdout.contains("wallet-policy-id-fingerprint:"),
"[{name}] canonical shape must KEEP wallet-policy-id-fingerprint; got {stdout:?}"
);
assert!(
!stdout.contains(ORIGIN_MARKER),
"[{name}] canonical shape must never show the origin marker"
);
}
}
#[cfg(feature = "json")]
#[test]
fn inspect_json_canonical_shapes_keep_wallet_policy_id_and_no_partial_key() {
for (name, template) in CANONICAL_SHAPES {
let phrase = encode(template, &[]);
let out = Command::cargo_bin("md")
.unwrap()
.args(["inspect", &phrase, "--json"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "[{name}] must stay exit 0");
let stdout = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(
v.get("wallet_policy_id").is_some(),
"[{name}] canonical shape must KEEP wallet_policy_id; got {v}"
);
assert!(
v.get("partial").is_none(),
"[{name}] canonical shape must NOT carry a partial key; got {v}"
);
}
}
fn multi_chunk_dead_template() -> String {
let n = 17;
let mut parts = vec!["pk(@0/<0;1>/*)".to_string()];
for i in 1..n {
parts.push(format!("s:pk(@{i}/<0;1>/*)"));
}
format!("wsh(thresh({n},{}))", parts.join(","))
}
#[test]
fn decode_text_multi_chunk_dead_card_partial_renders_and_exits_4() {
let template = multi_chunk_dead_template();
let chunks = encode_chunked(&template, &[]);
assert!(
chunks.len() >= 2,
"fixture must force 2+ chunks; got {}: {chunks:?}",
chunks.len()
);
let mut args = vec!["decode".to_string()];
args.extend(chunks.iter().cloned());
let out = Command::cargo_bin("md")
.unwrap()
.args(&args)
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(4),
"multi-chunk dead card must partial-decode with exit 4"
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains(&template),
"template line missing; got {stdout:?}"
);
assert!(
stdout.contains(ORIGIN_MARKER),
"origin marker missing; got {stdout:?}"
);
}
#[cfg(feature = "json")]
#[test]
fn decode_json_multi_chunk_dead_card_partial_has_reason() {
let template = multi_chunk_dead_template();
let chunks = encode_chunked(&template, &[]);
assert!(chunks.len() >= 2, "fixture must force 2+ chunks");
let mut args = vec!["decode".to_string()];
args.extend(chunks.iter().cloned());
args.push("--json".to_string());
let out = Command::cargo_bin("md")
.unwrap()
.args(&args)
.output()
.unwrap();
assert_eq!(out.status.code(), Some(4));
let stdout = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["partial"]["reason"], "missing_explicit_origin");
assert!(
!v["partial"]["unresolved_indices"]
.as_array()
.unwrap()
.is_empty()
);
}
#[test]
fn inspect_multi_chunk_dead_card_partial_renders_and_exits_4() {
let template = multi_chunk_dead_template();
let chunks = encode_chunked(&template, &[]);
assert!(chunks.len() >= 2, "fixture must force 2+ chunks");
let mut args = vec!["inspect".to_string()];
args.extend(chunks.iter().cloned());
let out = Command::cargo_bin("md")
.unwrap()
.args(&args)
.output()
.unwrap();
assert_eq!(out.status.code(), Some(4));
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains(ORIGIN_MARKER));
assert!(!stdout.contains("wallet-policy-id:"));
}
#[test]
fn decode_chunked_of_one_dead_card_partial_renders_and_exits_4() {
let chunks = encode_chunked("sh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*))", &[]);
assert_eq!(
chunks.len(),
1,
"small dead shape --force-chunked should still be a single chunk; got {chunks:?}"
);
let out = Command::cargo_bin("md")
.unwrap()
.args(["decode", &chunks[0]])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(4));
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains(ORIGIN_MARKER));
}