use std::path::Path;
use std::process::Command;
use assert_cmd::prelude::*;
use tempfile::TempDir;
fn mnem(repo: &Path, args: &[&str]) -> Command {
let mut cmd = Command::cargo_bin("mnem").expect("built mnem binary");
cmd.current_dir(repo);
cmd.arg("-R").arg(repo);
for a in args {
cmd.arg(a);
}
cmd
}
fn current_op_cid(repo: &Path) -> String {
let out = mnem(repo, &["log", "-n", "1"]).assert().success();
let stdout = String::from_utf8_lossy(&out.get_output().stdout).to_string();
for line in stdout.lines() {
if let Some(rest) = line.strip_prefix("op ") {
return rest.trim().to_string();
}
}
panic!("log -n 1 had no op line: {stdout}");
}
fn setup_with_node(dir: &TempDir) -> String {
mnem(dir.path(), &["init", dir.path().to_str().unwrap()])
.assert()
.success();
mnem(dir.path(), &["add", "node", "--summary", "cat-file test node", "--no-embed"])
.assert()
.success();
current_op_cid(dir.path())
}
fn head_commit_cid_from_op(repo: &Path, op_cid: &str) -> Option<String> {
let op_out = mnem(repo, &["cat-file", op_cid, "--json"])
.assert()
.success();
let op_json: serde_json::Value =
serde_json::from_slice(&op_out.get_output().stdout).expect("op --json must be valid JSON");
let view_cid = op_json
.get("view")
.and_then(|v| v.get("/"))
.and_then(|cid| cid.as_str())?
.to_string();
let view_out = mnem(repo, &["cat-file", &view_cid, "--json"])
.assert()
.success();
let view_json: serde_json::Value =
serde_json::from_slice(&view_out.get_output().stdout)
.expect("view --json must be valid JSON");
view_json
.get("heads")
.and_then(|h| h.as_array())
.and_then(|a| a.first())
.and_then(|link| link.get("/"))
.and_then(|cid| cid.as_str())
.map(|s| s.to_string())
}
#[test]
fn cat_file_raw_bytes_look_like_cbor_map() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init", dir.path().to_str().unwrap()])
.assert()
.success();
mnem(dir.path(), &["add", "node", "--summary", "a", "--no-embed"])
.assert()
.success();
let op = current_op_cid(dir.path());
let out = mnem(dir.path(), &["cat-file", &op]).assert().success();
let stdout = &out.get_output().stdout;
assert!(!stdout.is_empty(), "raw cat-file should emit bytes");
let first = stdout[0];
assert!(
(0xa0..=0xbf).contains(&first),
"expected CBOR map first byte (0xa0..0xbf), got {first:#x}"
);
}
#[test]
fn cat_file_json_carries_kind_field() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init", dir.path().to_str().unwrap()])
.assert()
.success();
mnem(dir.path(), &["add", "node", "--summary", "b", "--no-embed"])
.assert()
.success();
let op = current_op_cid(dir.path());
let out = mnem(dir.path(), &["cat-file", &op, "--json"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&out.get_output().stdout).to_string();
assert!(
stdout.contains("\"_kind\""),
"JSON must carry _kind discriminator, got: {stdout}"
);
assert!(
stdout.contains("\"operation\""),
"this op block must be of kind `operation`, got: {stdout}"
);
}
#[test]
fn cat_file_unknown_cid_errors() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init", dir.path().to_str().unwrap()])
.assert()
.success();
let missing = "bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc3pgi";
let out = mnem(dir.path(), &["cat-file", missing]).assert().failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("not present"),
"expected not-present error, got: {stderr}"
);
}
#[test]
fn cat_file_commit_object_raw_returns_cbor_map() {
let dir = TempDir::new().unwrap();
let op_cid = setup_with_node(&dir);
let commit_cid = head_commit_cid_from_op(dir.path(), &op_cid).expect(
"add node must produce a committed head; view.heads must be non-empty in the op JSON",
);
let out = mnem(dir.path(), &["cat-file", &commit_cid])
.assert()
.success();
let stdout = &out.get_output().stdout;
assert!(
!stdout.is_empty(),
"cat-file of a commit CID must emit bytes; CID={commit_cid}"
);
let first = stdout[0];
assert!(
(0xa0..=0xbf).contains(&first),
"Commit object is a CBOR map; expected first byte 0xa0..0xbf, \
got {first:#x}; CID={commit_cid}"
);
}
#[test]
fn cat_file_raw_has_no_trailing_newline_json_does() {
let dir = TempDir::new().unwrap();
let op_cid = setup_with_node(&dir);
let raw_out = mnem(dir.path(), &["cat-file", &op_cid])
.assert()
.success();
let raw_bytes = &raw_out.get_output().stdout;
assert!(
!raw_bytes.is_empty(),
"raw cat-file output must not be empty"
);
assert_ne!(
*raw_bytes.last().unwrap(),
b'\n',
"raw cat-file must NOT append a trailing newline; \
last byte is {:#x}, full length {} bytes",
raw_bytes.last().unwrap(),
raw_bytes.len()
);
let json_out = mnem(dir.path(), &["cat-file", &op_cid, "--json"])
.assert()
.success();
let json_bytes = &json_out.get_output().stdout;
assert_eq!(
*json_bytes.last().unwrap(),
b'\n',
"cat-file --json must append a trailing newline; \
last byte is {:#x}, full length {} bytes",
json_bytes.last().unwrap(),
json_bytes.len()
);
}
#[test]
fn cat_file_invalid_cid_syntax_errors_distinctly_from_missing() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init", dir.path().to_str().unwrap()])
.assert()
.success();
let out = mnem(dir.path(), &["cat-file", "not-a-cid"])
.assert()
.failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("invalid CID") || stderr.contains("invalid"),
"invalid syntax must produce a parse-error mentioning 'invalid'; got: {stderr}"
);
assert!(
!stderr.contains("not present"),
"syntax error must be distinct from the missing-block error; got: {stderr}"
);
}
#[test]
fn cat_file_json_is_parseable_object_with_string_kind() {
let dir = TempDir::new().unwrap();
let op_cid = setup_with_node(&dir);
let out = mnem(dir.path(), &["cat-file", &op_cid, "--json"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&out.get_output().stdout).to_string();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.expect("cat-file --json must produce parseable JSON");
assert!(
parsed.is_object(),
"cat-file --json must produce a JSON object (top-level), got: {parsed:?}"
);
let kind = parsed
.get("_kind")
.and_then(|v| v.as_str())
.expect("_kind field must be present and a string");
assert!(
!kind.is_empty(),
"_kind must be a non-empty string; got empty string"
);
}