use super::*;
use crate::core::types::{Resource, ResourceType};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn resource(path: &str, dest: &str, dirs: &[&str]) -> Resource {
let mut r = Resource {
resource_type: ResourceType::NasArchive,
path: Some(path.to_string()),
..Default::default()
};
r.archive.destination = Some(dest.to_string());
r.archive.dirs = dirs.iter().map(|s| s.to_string()).collect();
r.archive.min_age_days = Some(0); r
}
struct Scratch(PathBuf);
impl Scratch {
fn new(tag: &str) -> Self {
let p = std::env::temp_dir().join(format!(
"forjar-nas-archive-{tag}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
Self(p)
}
fn join(&self, s: &str) -> PathBuf {
self.0.join(s)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn write(p: &Path, body: &str) {
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, body).unwrap();
}
fn rsync_or_skip(test: &str) -> bool {
if Command::new("rsync").arg("--version").output().is_ok() {
return true;
}
eprintln!(
"SKIP {test}: rsync is not installed — the verify-before-delete guard cannot be exercised"
);
false
}
fn run(script: &str, execute: bool) -> (i32, String) {
let dir = std::env::temp_dir().join(format!(
"forjar-arch-run-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
fs::create_dir_all(&dir).unwrap();
let f = dir.join("archive.sh");
fs::write(&f, script).unwrap();
let out = Command::new("bash")
.arg(&f)
.env("ARCHIVE_EXECUTE", if execute { "1" } else { "0" })
.output()
.expect("bash");
let _ = fs::remove_dir_all(&dir);
let mut text = String::from_utf8_lossy(&out.stdout).to_string();
text.push_str(&String::from_utf8_lossy(&out.stderr));
(out.status.code().unwrap_or(-1), text)
}
#[test]
fn the_generated_script_passes_forjars_own_i8_gate() {
let a = archive_of(&resource("/mnt/nvme-raid0", "/mnt/unas/media", &["corpus"])).unwrap();
let script = archive_script(&a);
if let Err(e) = crate::core::purifier::validate_script(&script) {
panic!("nas_archive generates a script forjar itself rejects:\n{e}");
}
}
#[test]
fn the_check_and_apply_scripts_pass_the_i8_gate() {
let r = resource(
"/mnt/nvme-raid0",
"/mnt/unas/media",
&["corpus", "albor-data"],
);
for (name, s) in [
("check", check_script(&r)),
("apply", apply_script(&r)),
("state_query", state_query_script(&r)),
] {
if let Err(e) = crate::core::purifier::validate_script(&s) {
panic!("nas_archive {name}_script fails forjar's I8 gate:\n{s}\n\n{e}");
}
}
}
#[test]
fn a_dry_run_moves_nothing() {
if !rsync_or_skip("a_dry_run_moves_nothing") {
return;
}
let s = Scratch::new("dry");
let (src, dest) = (s.join("src"), s.join("dest"));
write(&src.join("corpus/a.bin"), &"x".repeat(200_000));
fs::create_dir_all(&dest).unwrap();
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["corpus"],
))
.unwrap();
let (code, out) = run(&archive_script(&a), false);
assert_eq!(code, 0, "{out}");
assert!(out.contains("DRY-RUN would archive corpus"), "{out}");
assert!(src.join("corpus/a.bin").exists(), "dry run moved data");
assert!(
!dest.join("corpus").exists(),
"dry run wrote the destination"
);
}
#[test]
fn an_archived_directory_is_verified_then_replaced_by_a_symlink() {
if !rsync_or_skip("an_archived_directory_is_verified_then_replaced_by_a_symlink") {
return;
}
let s = Scratch::new("move");
let (src, dest) = (s.join("src"), s.join("dest"));
write(&src.join("corpus/a.bin"), &"a".repeat(200_000));
write(&src.join("corpus/nested/b.bin"), &"b".repeat(200_000));
fs::create_dir_all(&dest).unwrap();
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["corpus"],
))
.unwrap();
let (code, out) = run(&archive_script(&a), true);
assert_eq!(code, 0, "{out}");
assert!(out.contains("VERIFIED corpus"), "{out}");
assert!(out.contains("archived=1"), "{out}");
assert_eq!(
fs::read(dest.join("corpus/a.bin")).unwrap().len(),
200_000,
"destination content is wrong"
);
assert!(dest.join("corpus/nested/b.bin").exists());
let meta = fs::symlink_metadata(src.join("corpus")).unwrap();
assert!(meta.file_type().is_symlink(), "no symlink left behind");
assert_eq!(
fs::canonicalize(src.join("corpus")).unwrap(),
fs::canonicalize(dest.join("corpus")).unwrap()
);
}
#[test]
fn a_second_pass_is_a_no_op() {
if !rsync_or_skip("a_second_pass_is_a_no_op") {
return;
}
let s = Scratch::new("idem");
let (src, dest) = (s.join("src"), s.join("dest"));
write(&src.join("corpus/a.bin"), &"a".repeat(100_000));
fs::create_dir_all(&dest).unwrap();
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["corpus"],
))
.unwrap();
let (c1, _) = run(&archive_script(&a), true);
assert_eq!(c1, 0);
let (c2, out2) = run(&archive_script(&a), true);
assert_eq!(c2, 0, "{out2}");
assert!(out2.contains("already a symlink"), "{out2}");
assert!(out2.contains("archived=0"), "{out2}");
}
#[test]
fn a_destination_that_does_not_match_is_never_deleted_from() {
if !rsync_or_skip("a_destination_that_does_not_match_is_never_deleted_from") {
return;
}
let s = Scratch::new("mismatch");
let (src, dest) = (s.join("src"), s.join("dest"));
write(&src.join("corpus/a.bin"), &"a".repeat(100_000));
write(&dest.join("corpus/intruder.bin"), &"z".repeat(100_000));
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["corpus"],
))
.unwrap();
let (code, out) = run(&archive_script(&a), true);
assert_ne!(code, 0, "a mismatched destination did not fail:\n{out}");
assert!(out.contains("VERIFY FAILED"), "{out}");
assert!(
src.join("corpus/a.bin").exists(),
"THE SOURCE WAS DELETED after a failed verify:\n{out}"
);
}
#[test]
fn an_unwritable_destination_stops_before_touching_anything() {
if !rsync_or_skip("an_unwritable_destination_stops_before_touching_anything") {
return;
}
let s = Scratch::new("unwritable");
let (src, dest) = (s.join("src"), s.join("dest"));
write(&src.join("corpus/a.bin"), &"a".repeat(100_000));
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["corpus"],
))
.unwrap();
let (code, out) = run(&archive_script(&a), true);
assert_ne!(
code, 0,
"a missing destination did not fail — the pass would proceed toward a delete:\n{out}"
);
assert!(
src.join("corpus/a.bin").exists(),
"THE SOURCE WAS TOUCHED despite an unusable destination:\n{out}"
);
assert_eq!(
std::fs::read_to_string(src.join("corpus/a.bin"))
.unwrap()
.len(),
100_000,
"the source was modified in place:\n{out}"
);
}
#[test]
fn a_tree_whose_small_files_exceed_the_budget_is_refused() {
if !rsync_or_skip("a_tree_whose_small_files_exceed_the_budget_is_refused") {
return;
}
let s = Scratch::new("small");
let (src, dest) = (s.join("src"), s.join("dest"));
for i in 0..40 {
write(&src.join(format!("corpus/f{i}.bin")), &"s".repeat(32_768));
}
fs::create_dir_all(&dest).unwrap();
let mut r = resource(src.to_str().unwrap(), dest.to_str().unwrap(), &["corpus"]);
r.archive.max_small_bytes = Some(1_000_000); let a = archive_of(&r).unwrap();
let (code, out) = run(&archive_script(&a), true);
assert_eq!(code, 0, "{out}");
assert!(out.contains("exceeds archive_max_small_bytes"), "{out}");
assert!(src.join("corpus/f0.bin").exists(), "refused tree was moved");
}
#[test]
fn a_large_tree_with_a_few_small_files_is_archived() {
if !rsync_or_skip("a_large_tree_with_a_few_small_files_is_archived") {
return;
}
let s = Scratch::new("courses");
let (src, dest) = (s.join("src"), s.join("dest"));
for i in 0..6 {
write(
&src.join(format!("courses/big{i}.bin")),
&"b".repeat(200_000),
);
write(&src.join(format!("courses/meta{i}.txt")), "transcript");
}
fs::create_dir_all(&dest).unwrap();
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["courses"],
))
.unwrap();
let (code, out) = run(&archive_script(&a), true);
assert_eq!(code, 0, "{out}");
assert!(
out.contains("VERIFIED courses"),
"a large tree with a few small files was refused:\n{out}"
);
assert!(dest.join("courses/big0.bin").exists());
}
#[test]
fn a_handful_of_small_files_is_not_refused() {
if !rsync_or_skip("a_handful_of_small_files_is_not_refused") {
return;
}
let s = Scratch::new("fewsmall");
let (src, dest) = (s.join("src"), s.join("dest"));
for i in 0..7 {
write(&src.join(format!("cp/s{i}.txt")), "tiny");
}
write(&src.join("cp/big.bin"), &"b".repeat(200_000));
fs::create_dir_all(&dest).unwrap();
let a = archive_of(&resource(
src.to_str().unwrap(),
dest.to_str().unwrap(),
&["cp"],
))
.unwrap();
let (code, out) = run(&archive_script(&a), true);
assert_eq!(code, 0, "{out}");
assert!(
out.contains("VERIFIED cp"),
"a small directory was refused:\n{out}"
);
}
#[test]
fn state_query_names_every_declared_directory() {
let q = state_query_script(&resource(
"/mnt/nvme-raid0",
"/mnt/unas/media",
&["corpus", "albor-data", "gemma2-models"],
));
for d in ["corpus", "albor-data", "gemma2-models"] {
assert!(q.contains(d), "state query omits {d}:\n{q}");
}
}
#[test]
fn a_bad_declaration_is_refused_by_every_entry_point() {
let r = resource("/mnt/raid", "/mnt/raid/archive", &["a"]);
for s in [check_script(&r), apply_script(&r), state_query_script(&r)] {
assert!(
s.contains("ERROR:"),
"a bad declaration produced a live script:\n{s}"
);
assert!(s.contains("exit 1"), "{s}");
}
}
#[test]
fn the_check_script_reports_each_directory_separately() {
let c = check_script(&resource(
"/mnt/nvme-raid0",
"/mnt/unas/media",
&["corpus", "albor-data"],
));
assert!(
c.contains("archived:corpus") || c.contains("'archived:corpus'"),
"{c}"
);
assert!(c.contains("archive-pending:albor-data"), "{c}");
}
#[test]
fn a_declaration_without_a_path_is_refused() {
let mut r = Resource {
resource_type: ResourceType::NasArchive,
..Default::default()
};
r.archive.destination = Some("/mnt/unas/media".to_string());
r.archive.dirs = vec!["corpus".to_string()];
let e = archive_of(&r).unwrap_err();
assert!(e.contains("requires `path`"), "{e}");
assert!(check_script(&r).contains("ERROR:"));
assert!(apply_script(&r).contains("ERROR:"));
assert!(state_query_script(&r).contains("ERROR:"));
}
#[test]
fn the_unit_names_are_derived_from_the_path_and_never_empty() {
assert_eq!(slug("/"), "root");
assert_eq!(slug("///"), "root");
assert_eq!(slug("/mnt/nvme-raid0"), "mnt-nvme-raid0");
assert!(script_path("/").ends_with("forjar-archive-root.sh"));
assert_eq!(
service_name("/mnt/nvme-raid0"),
"forjar-archive-mnt-nvme-raid0"
);
}
#[test]
fn the_installed_units_run_the_script_and_survive_a_missed_window() {
let r = resource("/mnt/nvme-raid0", "/mnt/unas/media", &["corpus"]);
let apply = apply_script(&r);
assert!(apply.contains("ARCHIVE_EXECUTE=1"), "{apply}");
assert!(
apply.contains("/usr/local/sbin/forjar-archive-mnt-nvme-raid0.sh"),
"{apply}"
);
assert!(apply.contains("Persistent=true"), "{apply}");
assert!(apply.contains("systemctl enable --now"), "{apply}");
}
#[test]
fn a_missing_tool_is_refused_before_the_destination_is_touched() {
let a = archive_of(&resource("/mnt/nvme-raid0", "/mnt/unas/media", &["corpus"])).unwrap();
let script = archive_script(&a);
let preflight = script
.find("command -v \"$tool\"")
.expect("no tool preflight");
let first_mkdir = script.find("mkdir -p").expect("no mkdir");
assert!(
preflight < first_mkdir,
"the tool check runs AFTER the destination is created, so a machine \
without rsync gets a half-made destination and then a missing-binary error"
);
}
#[test]
fn the_environment_can_exercise_the_safety_property() {
assert!(
Command::new("rsync").arg("--version").output().is_ok(),
"rsync is absent, so every test that RUNS the archive script skipped — \
the verify-before-delete guard was not exercised at all. This suite \
cannot certify the one property that protects data without it."
);
}