use assert_cmd::Command;
use std::process::Command as StdCommand;
const PRIVATE_KEY_LINE: &str = "warning: stdout carries private key material (can spend) \u{2014} redirect or encrypt (e.g. '> file.txt' or '| age -e ...')";
const WATCH_ONLY_LINE: &str = "note: stdout is watch-only \u{2014} public keys only, cannot spend";
const TEMPLATE_LINE: &str = "note: stdout is a keyless descriptor template (no keys)";
const MD1_FIXTURE: &str = "md1yqpqqxqq8xtwhw4xwn4qh";
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
#[test]
fn byte_parity_advisory_lines() {
assert_eq!(PRIVATE_KEY_LINE, "warning: stdout carries private key material (can spend) \u{2014} redirect or encrypt (e.g. '> file.txt' or '| age -e ...')");
assert_eq!(WATCH_ONLY_LINE, "note: stdout is watch-only \u{2014} public keys only, cannot spend");
assert_eq!(TEMPLATE_LINE, "note: stdout is a keyless descriptor template (no keys)");
}
#[test]
fn decode_text_emits_template_advisory() {
let out = Command::cargo_bin("md").unwrap().args(["decode", MD1_FIXTURE]).output().unwrap();
assert!(out.status.success());
assert!(String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE));
}
#[test]
fn decode_json_emits_template_advisory() {
let out = Command::cargo_bin("md").unwrap().args(["decode", "--json", MD1_FIXTURE]).output().unwrap();
assert!(out.status.success());
assert!(String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE), "json branch missed the advisory");
}
#[test]
fn encode_text_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["encode", "wpkh(@0/<0;1>/*)"])
.output()
.unwrap();
assert!(out.status.success(), "encode exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"encode text: expected TEMPLATE_LINE on stderr"
);
}
#[test]
fn encode_json_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["encode", "--json", "wpkh(@0/<0;1>/*)"])
.output()
.unwrap();
assert!(out.status.success(), "encode --json exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"encode json: expected TEMPLATE_LINE on stderr"
);
}
#[test]
fn inspect_text_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["inspect", MD1_FIXTURE])
.output()
.unwrap();
assert!(out.status.success(), "inspect exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"inspect text: expected TEMPLATE_LINE on stderr"
);
}
#[test]
fn inspect_json_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["inspect", "--json", MD1_FIXTURE])
.output()
.unwrap();
assert!(out.status.success(), "inspect --json exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"inspect json: expected TEMPLATE_LINE on stderr"
);
}
#[test]
fn bytecode_text_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["bytecode", MD1_FIXTURE])
.output()
.unwrap();
assert!(out.status.success(), "bytecode exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"bytecode text: expected TEMPLATE_LINE on stderr"
);
}
#[test]
fn bytecode_json_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["bytecode", "--json", MD1_FIXTURE])
.output()
.unwrap();
assert!(out.status.success(), "bytecode --json exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"bytecode json: expected TEMPLATE_LINE on stderr"
);
}
fn encode_chunked_for_repair(template: &str) -> Vec<String> {
let out = StdCommand::new(assert_cmd::cargo::cargo_bin("md"))
.args(["encode", "--force-chunked", template])
.output()
.expect("invoke md encode --force-chunked");
assert!(
out.status.success(),
"md encode --force-chunked {template:?} failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let s = String::from_utf8(out.stdout).expect("stdout utf-8");
s.lines()
.filter(|l| l.starts_with("md1"))
.map(String::from)
.collect()
}
fn corrupt_at_for_repair(chunk: &str, pos: usize, xor_mask: u8) -> String {
let hrp_len = 3; let mut chars: Vec<char> = chunk.chars().collect();
let abs_idx = hrp_len + pos;
let original_sym = CODEX32_ALPHABET
.iter()
.position(|&b| b == chars[abs_idx].to_ascii_lowercase() as u8)
.expect("char in codex32 alphabet") as u8;
let new_sym = (original_sym ^ (xor_mask & 0x1F)) & 0x1F;
chars[abs_idx] = CODEX32_ALPHABET[new_sym as usize] as char;
chars.iter().collect()
}
#[test]
fn repair_emits_template() {
let chunks = encode_chunked_for_repair("wpkh(@0/<0;1>/*)");
assert_eq!(chunks.len(), 1, "single-chunk fixture must produce exactly 1 chunk; got {chunks:?}");
let valid = &chunks[0];
let corrupted = corrupt_at_for_repair(valid, 10, 0b10110);
let out = Command::cargo_bin("md")
.unwrap()
.args(["repair", &corrupted])
.output()
.expect("invoke md repair");
assert_eq!(
out.status.code(),
Some(5),
"expected exit 5 (REPAIR_APPLIED); stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"repair: expected TEMPLATE_LINE on stderr"
);
}
fn account_xpub(path: &str) -> String {
use bitcoin::Network;
use bitcoin::bip32::{DerivationPath, Xpriv, Xpub};
use bitcoin::secp256k1::Secp256k1;
use std::str::FromStr;
const ABANDON: &str =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let mn = bip39::Mnemonic::parse(ABANDON).unwrap();
let seed = mn.to_seed("");
let secp = Secp256k1::new();
let master = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
let dp = DerivationPath::from_str(path).unwrap();
let xpriv = master.derive_priv(&secp, &dp).unwrap();
Xpub::from_priv(&secp, &xpriv).to_string()
}
#[test]
fn address_text_emits_watch_only() {
let xpub = account_xpub("m/84'/0'/0'");
let key_arg = format!("@0={xpub}");
let out = Command::cargo_bin("md")
.unwrap()
.args(["address", "--template", "wpkh(@0/<0;1>/*)", "--key", &key_arg])
.output()
.unwrap();
assert!(out.status.success(), "address exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(WATCH_ONLY_LINE),
"address text: expected WATCH_ONLY_LINE on stderr (not template)"
);
}
#[test]
fn address_json_emits_watch_only() {
let xpub = account_xpub("m/84'/0'/0'");
let key_arg = format!("@0={xpub}");
let out = Command::cargo_bin("md")
.unwrap()
.args(["address", "--json", "--template", "wpkh(@0/<0;1>/*)", "--key", &key_arg])
.output()
.unwrap();
assert!(out.status.success(), "address --json exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(WATCH_ONLY_LINE),
"address json: expected WATCH_ONLY_LINE on stderr (not template)"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn compile_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["compile", "pk(@0)", "--context", "segwitv0"])
.output()
.unwrap();
assert!(out.status.success(), "compile exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"compile text: expected TEMPLATE_LINE on stderr"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn compile_json_emits_template() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["compile", "--json", "pk(@0)", "--context", "segwitv0"])
.output()
.unwrap();
assert!(out.status.success(), "compile --json exited non-zero; stderr={}", String::from_utf8_lossy(&out.stderr));
assert!(
String::from_utf8(out.stderr).unwrap().contains(TEMPLATE_LINE),
"compile json: expected TEMPLATE_LINE on stderr"
);
}
#[test]
fn repair_error_path_emits_no_advisory() {
let irreparably_corrupt = "md1zqzqqzqqzztzhzzzznzzz";
let out = Command::cargo_bin("md")
.unwrap()
.args(["repair", irreparably_corrupt])
.output()
.expect("invoke md repair");
assert_eq!(
out.status.code(),
Some(2),
"expected exit 2 (irrecoverable); stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert_no_advisory(&String::from_utf8(out.stderr).unwrap());
}
fn assert_no_advisory(stderr: &str) {
for line in [PRIVATE_KEY_LINE, WATCH_ONLY_LINE, TEMPLATE_LINE] {
assert!(!stderr.contains(line), "inert command emitted an advisory: {stderr}");
}
}
#[test]
fn verify_emits_no_advisory() {
let out = Command::cargo_bin("md")
.unwrap()
.args(["verify", MD1_FIXTURE, "--template", "wpkh(@0/<0;1>/*)"])
.output()
.unwrap();
assert_no_advisory(&String::from_utf8(out.stderr).unwrap());
}
#[test]
fn vectors_emits_no_advisory() {
let dir = tempfile::tempdir().unwrap();
let out = Command::cargo_bin("md")
.unwrap()
.args(["vectors", "--out", dir.path().to_str().unwrap()])
.output()
.unwrap();
assert_no_advisory(&String::from_utf8(out.stderr).unwrap());
}
#[cfg(feature = "json")]
#[test]
fn gui_schema_emits_no_advisory() {
let out = Command::cargo_bin("md").unwrap().arg("gui-schema").output().unwrap();
assert_no_advisory(&String::from_utf8(out.stderr).unwrap());
}