mod common;
#[test]
fn batch_write_creates_file() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("batch_out.txt");
let manifest = common::manifest(&[serde_json::json!({
"op": "write",
"target": target.to_string_lossy(),
"content": "hello batch",
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let events = common::parse_ndjson(&output.stdout);
let op = events
.iter()
.find(|e| e["type"] == "batch_op")
.expect("batch_op event");
assert_eq!(op["op"], "write");
assert_eq!(op["status"], "ok");
let summary = events
.iter()
.find(|e| e["type"] == "summary")
.expect("summary");
assert_eq!(summary["operations"], 1);
assert_eq!(summary["succeeded"], 1);
assert_eq!(summary["failed"], 0);
let content = std::fs::read_to_string(&target).expect("read");
assert_eq!(content, "hello batch");
}
#[test]
fn batch_replace_modifies_file() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("replace_me.txt");
std::fs::write(&target, "old_value here\n").expect("write");
let manifest = common::manifest(&[serde_json::json!({
"op": "replace",
"target": target.to_string_lossy(),
"pattern": "old_value",
"replacement": "new_value",
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(output.status.success());
let content = std::fs::read_to_string(&target).expect("read");
assert!(content.contains("new_value"));
assert!(!content.contains("old_value"));
}
#[test]
fn batch_delete_removes_file() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("to_delete.txt");
std::fs::write(&target, "delete me\n").expect("write");
let manifest = common::manifest(&[serde_json::json!({
"op": "delete",
"target": target.to_string_lossy(),
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(output.status.success());
assert!(!target.exists());
}
#[test]
fn batch_dry_run_does_not_modify() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("keep.txt");
let original = "keep me\n";
std::fs::write(&target, original).expect("write");
let manifest = common::manifest(&[serde_json::json!({
"op": "replace",
"target": target.to_string_lossy(),
"pattern": "keep",
"replacement": "gone",
})]);
let output = common::atomwrite()
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--dry-run",
])
.write_stdin(manifest)
.output()
.expect("run");
assert!(output.status.success());
let content = std::fs::read_to_string(&target).expect("read");
assert_eq!(content, original);
}
#[test]
fn batch_multiple_operations() {
let dir = tempfile::tempdir().expect("tempdir");
let file_a = dir.path().join("a.txt");
let file_b = dir.path().join("b.txt");
std::fs::write(&file_b, "original_b\n").expect("write");
let manifest = common::manifest(&[
serde_json::json!({
"op": "write",
"target": file_a.to_string_lossy(),
"content": "content_a",
}),
serde_json::json!({
"op": "replace",
"target": file_b.to_string_lossy(),
"pattern": "original_b",
"replacement": "modified_b",
}),
]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(output.status.success());
let events = common::parse_ndjson(&output.stdout);
let summary = events
.iter()
.find(|e| e["type"] == "summary")
.expect("summary");
assert_eq!(summary["operations"], 2);
assert_eq!(summary["succeeded"], 2);
assert_eq!(std::fs::read_to_string(&file_a).expect("a"), "content_a");
assert!(
std::fs::read_to_string(&file_b)
.expect("b")
.contains("modified_b")
);
}
#[test]
fn batch_invalid_op_fails() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = r#"{"op":"nonexistent","target":"foo.txt"}"#;
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(!output.status.success());
let events = common::parse_ndjson(&output.stdout);
let op = events
.iter()
.find(|e| e["type"] == "batch_op")
.expect("batch_op");
assert_eq!(op["status"], "failed");
}
#[test]
fn batch_empty_manifest_fails() {
let dir = tempfile::tempdir().expect("tempdir");
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin("")
.output()
.expect("run");
assert!(!output.status.success());
}
#[test]
fn batch_move_with_source_target() {
let dir = tempfile::tempdir().expect("tempdir");
let src = common::create_test_file(dir.path(), "origin.txt", "move me\n");
let dest = dir.path().join("destination.txt");
let manifest = common::manifest(&[serde_json::json!({
"op": "move",
"source": src.to_string_lossy(),
"target": dest.to_string_lossy(),
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!src.exists(), "source should be removed after move");
assert!(dest.exists(), "destination should exist after move");
assert_eq!(std::fs::read_to_string(&dest).unwrap(), "move me\n");
}
#[test]
fn batch_copy_with_source_target() {
let dir = tempfile::tempdir().expect("tempdir");
let src = common::create_test_file(dir.path(), "src_copy.txt", "copy me\n");
let dest = dir.path().join("dst_copy.txt");
let manifest = common::manifest(&[serde_json::json!({
"op": "copy",
"source": src.to_string_lossy(),
"target": dest.to_string_lossy(),
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(src.exists(), "source should still exist after copy");
assert!(dest.exists(), "destination should exist after copy");
assert_eq!(std::fs::read_to_string(&dest).unwrap(), "copy me\n");
}
#[test]
fn batch_move_with_from_alias() {
let dir = tempfile::tempdir().expect("tempdir");
let src = common::create_test_file(dir.path(), "from_file.txt", "alias test\n");
let dest = dir.path().join("to_file.txt");
let manifest = common::manifest(&[serde_json::json!({
"op": "move",
"from": src.to_string_lossy(),
"target": dest.to_string_lossy(),
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!src.exists());
assert_eq!(std::fs::read_to_string(&dest).unwrap(), "alias test\n");
}
#[test]
fn batch_write_with_path_alias() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("path_alias.txt");
let manifest = common::manifest(&[serde_json::json!({
"op": "write",
"path": target.to_string_lossy(),
"content": "via path field",
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(std::fs::read_to_string(&target).unwrap(), "via path field");
}
#[test]
fn batch_delete_with_path_alias() {
let dir = tempfile::tempdir().expect("tempdir");
let target = common::create_test_file(dir.path(), "to_del.txt", "del me\n");
let manifest = common::manifest(&[serde_json::json!({
"op": "delete",
"path": target.to_string_lossy(),
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!target.exists(), "file should be deleted via path alias");
}
#[test]
fn batch_move_legacy_compat() {
let dir = tempfile::tempdir().expect("tempdir");
let src = common::create_test_file(dir.path(), "legacy_src.txt", "legacy\n");
let dest = dir.path().join("legacy_dst.txt");
let manifest = common::manifest(&[serde_json::json!({
"op": "move",
"path": src.to_string_lossy(),
"target": dest.to_string_lossy(),
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn batch_transaction_rollback_preserves_created_files() {
let dir = tempfile::tempdir().expect("tempdir");
let created = dir.path().join("created_in_tx.txt");
let existing = common::create_test_file(dir.path(), "existing.txt", "original\n");
let manifest = common::manifest(&[
serde_json::json!({
"op": "write",
"target": created.to_string_lossy(),
"content": "new file",
}),
serde_json::json!({
"op": "replace",
"target": dir.path().join("does_not_exist.txt").to_string_lossy(),
"pattern": "nonexistent_pattern_xyz",
"replacement": "x",
}),
]);
let _output = common::atomwrite()
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--transaction",
])
.write_stdin(manifest)
.output()
.expect("run");
let existing_content = std::fs::read_to_string(&existing).expect("read existing");
assert_eq!(
existing_content, "original\n",
"existing file should be restored by rollback"
);
if created.exists() {
eprintln!(
"NOTE: known limitation — file created during transaction persists after rollback"
);
}
}
#[test]
fn batch_write_escapes_backslash_in_target_path() {
let dir = tempfile::tempdir().expect("tempdir");
let target = format!("{}/with\\backslash.txt", dir.path().display());
let manifest = common::manifest(&[serde_json::json!({
"op": "write",
"target": target,
"content": "backslash ok",
})]);
let output = common::atomwrite()
.args(["--workspace", dir.path().to_str().unwrap(), "batch"])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"backslash in target path must be JSON-escaped; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let events = common::parse_ndjson(&output.stdout);
let op = events
.iter()
.find(|e| e["type"] == "batch_op")
.expect("batch_op event");
assert_eq!(op["status"], "ok");
}