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: secret material on stdout"),
"expected D9 secret-on-stdout advisory on stderr; got {stderr:?}"
);
}
#[test]
fn repair_one_substitution_exits_5() {
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,
5,
"expected exit 5 (REPAIR_APPLIED); 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: secret material on stdout"),
"expected D9 secret-on-stdout advisory on stderr; 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,
5,
"expected exit 5 for JSON-mode repair; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
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["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\""
);
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: secret material on stdout"),
"expected D9 secret-on-stdout advisory on stderr in JSON mode; got {stderr:?}"
);
}
#[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,
5,
"expected exit 5 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:?}"
);
}