use std::process::{Command, Stdio};
use assert_cmd::cargo::CommandCargoExt;
const ABANDON_MS1: &str = "ms10entrsqqqqqqqqqqqqqqqqqqqqqqqqqqqqcj9sxraq34v7f";
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
fn flip_at(chunk: &str, pos: usize) -> String {
let (prefix, rest) = chunk.split_at(3);
let mut chars: Vec<char> = rest.chars().collect();
let was = chars[pos];
let alphabet_str = std::str::from_utf8(CODEX32_ALPHABET).unwrap();
let was_idx = alphabet_str.find(was).unwrap();
let new_idx = (was_idx + 1) % 32;
chars[pos] = alphabet_str.chars().nth(new_idx).unwrap();
let mut out = String::from(prefix);
for c in chars {
out.push(c);
}
out
}
fn flip_many(chunk: &str, positions: &[usize]) -> String {
positions
.iter()
.fold(chunk.to_string(), |acc, &p| flip_at(&acc, p))
}
#[test]
fn repair_already_valid_input_exits_0() {
let mut cmd = Command::cargo_bin("ms").expect("ms binary");
let out = cmd
.args(["repair", "--ms1", ABANDON_MS1])
.output()
.expect("invoke ms repair");
let code = out.status.code().expect("exited normally");
assert_eq!(
code,
0,
"expected exit 0 for clean input; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("stdout utf-8");
assert!(
!stdout.contains("# Repair report"),
"clean input must not emit a Repair report; got stdout={stdout:?}"
);
assert!(
stdout.lines().any(|line| line == ABANDON_MS1),
"expected pass-through of valid input on stdout; got {stdout:?}"
);
let stderr = String::from_utf8(out.stderr).expect("stderr utf-8");
assert!(
stderr.contains("warning: stdout carries private key material"),
"expected D9 output-class advisory on stderr; got {stderr:?}"
);
}
#[test]
fn repair_one_substitution_exits_4_candidate() {
let corrupted = flip_at(ABANDON_MS1, 9);
let mut cmd = Command::cargo_bin("ms").expect("ms binary");
let out = cmd
.args(["repair", "--ms1", &corrupted])
.output()
.expect("invoke ms repair");
let code = out.status.code().expect("exited normally");
assert_eq!(
code,
4,
"expected exit 4 (VERIFY-ME candidate — Cycle F demotion); stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("stdout utf-8");
assert!(
stdout.contains("# Repair report"),
"expected `# Repair report` header; got {stdout:?}"
);
assert!(
stdout.contains("ms1 chunk 0: 1 correction at position 9"),
"expected per-chunk correction line at position 9; got {stdout:?}"
);
assert!(
stdout.lines().any(|line| line == ABANDON_MS1),
"expected corrected chunk to match the original valid ms1; got {stdout:?}"
);
let stderr = String::from_utf8(out.stderr).expect("stderr utf-8");
assert!(
stderr.contains("warning: stdout carries private key material"),
"expected D9 output-class advisory on stderr; got {stderr:?}"
);
assert!(
stderr.contains("correction UNVERIFIED"),
"expected the UNVERIFIED advisory on stderr; got {stderr:?}"
);
assert!(
stderr.contains("self-verified"),
"expected the self-verification caveat on stderr; got {stderr:?}"
);
assert!(
stderr.contains("BIP-93"),
"expected the BIP-93 citation on stderr; got {stderr:?}"
);
}
#[test]
fn repair_clean_ms1_stays_exit_0_no_advisory() {
let mut cmd = Command::cargo_bin("ms").expect("ms binary");
let out = cmd
.args(["repair", "--ms1", ABANDON_MS1])
.output()
.expect("invoke ms repair");
let code = out.status.code().expect("exited normally");
assert_eq!(code, 0, "expected exit 0 for clean input");
let stderr = String::from_utf8(out.stderr).expect("stderr utf-8");
assert!(
!stderr.contains("correction UNVERIFIED"),
"clean input must not emit the UNVERIFIED advisory; got {stderr:?}"
);
}
#[test]
fn repair_unrepairable_exits_2() {
let irreparable = flip_many(ABANDON_MS1, &[3, 11, 19, 27, 35]);
let mut cmd = Command::cargo_bin("ms").expect("ms binary");
let out = cmd
.args(["repair", "--ms1", &irreparable])
.output()
.expect("invoke ms repair");
let code = out.status.code().expect("exited normally");
assert_eq!(
code,
2,
"expected exit 2 (FormatViolation::TooManyErrors); stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("uncorrectable") || stderr.contains("errors"),
"expected BCH-uncorrectable error message on stderr; got {stderr:?}"
);
}
#[test]
fn repair_json_envelope_shape() {
let corrupted = flip_at(ABANDON_MS1, 9);
let mut cmd = Command::cargo_bin("ms").expect("ms binary");
let out = cmd
.args(["repair", "--ms1", &corrupted, "--json"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("invoke ms repair --json");
let code = out.status.code().expect("exited normally");
assert_eq!(
code,
4,
"expected exit 4 (VERIFY-ME candidate) for JSON-mode repair; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("stdout utf-8");
let raw = stdout.trim();
let envelope: serde_json::Value = serde_json::from_str(raw).expect("stdout parses as JSON");
assert_eq!(
envelope["schema_version"],
serde_json::Value::String("1".into()),
"schema_version must equal \"1\" (string)"
);
assert_eq!(
envelope["kind"],
serde_json::Value::String("ms1".into()),
"kind must equal \"ms1\""
);
assert_eq!(
envelope["verdict"],
serde_json::Value::String("candidate".into()),
"verdict must equal \"candidate\" for a touched substitution correction"
);
let schema_version_pos = raw
.find("\"schema_version\"")
.expect("schema_version key present");
let kind_pos = raw.find("\"kind\"").expect("kind key present");
let verdict_pos = raw.find("\"verdict\"").expect("verdict key present");
let corrected_chunks_pos = raw
.find("\"corrected_chunks\"")
.expect("corrected_chunks key present");
let repairs_pos = raw.find("\"repairs\"").expect("repairs key present");
assert!(
schema_version_pos < kind_pos
&& kind_pos < verdict_pos
&& verdict_pos < corrected_chunks_pos
&& corrected_chunks_pos < repairs_pos,
"expected field order schema_version < kind < verdict < corrected_chunks < repairs; raw={raw:?}"
);
let corrected_chunks = envelope["corrected_chunks"]
.as_array()
.expect("corrected_chunks must be a JSON array");
assert_eq!(
corrected_chunks.len(),
1,
"ms1 single-chunk → one corrected_chunk"
);
assert_eq!(
corrected_chunks[0],
serde_json::Value::String(ABANDON_MS1.into()),
"corrected_chunk must equal the original valid ms1"
);
let repairs = envelope["repairs"]
.as_array()
.expect("repairs must be a JSON array");
assert_eq!(repairs.len(), 1, "one corrupted input → one repair entry");
let r0 = &repairs[0];
assert_eq!(
r0["chunk_index"],
serde_json::Value::from(0u32),
"ms1 single-chunk → chunk_index is always 0"
);
assert_eq!(
r0["original_chunk"],
serde_json::Value::String(corrupted.clone())
);
assert_eq!(
r0["corrected_chunk"],
serde_json::Value::String(ABANDON_MS1.into())
);
let positions = r0["corrected_positions"]
.as_array()
.expect("corrected_positions must be a JSON array");
assert_eq!(positions.len(), 1, "single-flip → one position entry");
let p0 = &positions[0];
assert_eq!(p0["position"], serde_json::Value::from(9u32));
assert!(p0["was"].is_string(), "was must be a string");
assert!(p0["now"].is_string(), "now must be a string");
assert_ne!(p0["was"], p0["now"], "was != now for a real correction");
let stderr = String::from_utf8(out.stderr).expect("stderr utf-8");
assert!(
stderr.contains("warning: stdout carries private key material"),
"expected D9 output-class advisory on stderr in JSON mode; got {stderr:?}"
);
}
#[test]
fn repair_json_clean_input_verdict_blessed() {
let mut cmd = Command::cargo_bin("ms").expect("ms binary");
let out = cmd
.args(["repair", "--ms1", ABANDON_MS1, "--json"])
.output()
.expect("invoke ms repair --json");
let code = out.status.code().expect("exited normally");
assert_eq!(code, 0, "expected exit 0 for clean JSON-mode repair");
let stdout = String::from_utf8(out.stdout).expect("stdout utf-8");
let envelope: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("stdout parses as JSON");
assert_eq!(
envelope["verdict"],
serde_json::Value::String("blessed".into()),
"verdict must equal \"blessed\" for a clean (uncorrected) card"
);
assert_eq!(envelope["kind"], serde_json::Value::String("ms1".into()));
let repairs = envelope["repairs"]
.as_array()
.expect("repairs must be a JSON array");
assert!(
repairs.is_empty(),
"clean input must have an empty repairs array; got {repairs:?}"
);
}
#[test]
fn repair_stdin_input_via_dash() {
use std::io::Write as _;
let corrupted = flip_at(ABANDON_MS1, 9);
let stdin_body = format!("{corrupted}\n");
let mut child = Command::cargo_bin("ms")
.expect("ms binary")
.args(["repair", "--ms1", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn ms repair --ms1 -");
child
.stdin
.as_mut()
.expect("stdin pipe")
.write_all(stdin_body.as_bytes())
.expect("write stdin");
let out = child.wait_with_output().expect("wait ms repair --ms1 -");
let code = out.status.code().expect("exited normally");
assert_eq!(
code,
4,
"expected exit 4 (VERIFY-ME candidate) for stdin-with-corrupted-input; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("stdout utf-8");
assert!(
stdout.lines().any(|line| line == ABANDON_MS1),
"expected restored ms1 on stdout; got {stdout:?}"
);
}