#![allow(clippy::needless_pass_by_value)]
use std::fs;
use std::path::{Path, PathBuf};
use grex_core::pack::{parse, Action, PackType};
use grex_core::sync::{self, SyncOptions};
use grex_core::ExecResult;
use tempfile::TempDir;
use tokio_util::sync::CancellationToken;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/grex must have a grand-parent (repo root)")
.to_path_buf()
}
fn template_root() -> PathBuf {
repo_root().join("examples").join("pack-template")
}
#[test]
fn pack_template_manifest_parses_and_matches_reference_shape() {
let manifest_path = template_root().join(".grex").join("pack.yaml");
let yaml = fs::read_to_string(&manifest_path)
.unwrap_or_else(|e| panic!("read {}: {e}", manifest_path.display()));
let pack = parse(&yaml).expect("pack-template/.grex/pack.yaml must parse clean");
assert_eq!(pack.schema_version.as_str(), "1", "schema_version must be \"1\"");
assert_eq!(pack.name, "grex-pack-template", "name drift");
assert_eq!(pack.r#type, PackType::Declarative, "type drift");
assert_eq!(pack.version.as_deref(), Some("1.0.0"), "version drift");
assert!(pack.children.is_empty(), "template does not use children");
assert!(
pack.actions.len() >= 2,
"template should have at least a require + one concrete action, got {}",
pack.actions.len()
);
assert!(matches!(pack.actions[0], Action::Require(_)), "first action must be a require gate");
let teardown = pack.teardown.as_ref().expect("explicit teardown expected");
assert_eq!(teardown.len(), 1, "teardown is a single rmdir");
assert!(matches!(teardown[0], Action::Rmdir(_)), "teardown step must be rmdir");
}
#[test]
fn pack_template_ships_all_expected_files() {
let root = template_root();
for rel in &[".grex/pack.yaml", "files/hello.txt", "README.md", ".gitignore"] {
let p = root.join(rel);
assert!(p.is_file(), "missing expected file: {}", p.display());
}
}
#[test]
fn pack_template_payload_referenced_by_symlink_exists() {
let payload = template_root().join("files").join("hello.txt");
assert!(payload.is_file(), "symlink.src target missing: {}", payload.display());
}
fn copy_dir(src: &Path, dst: &Path) {
fs::create_dir_all(dst).expect("create dst");
for entry in fs::read_dir(src).expect("read_dir") {
let entry = entry.expect("dir entry");
let ft = entry.file_type().expect("file_type");
let from = entry.path();
let to = dst.join(entry.file_name());
if ft.is_dir() {
copy_dir(&from, &to);
} else if ft.is_file() {
fs::copy(&from, &to).expect("copy file");
}
}
}
#[test]
fn pack_template_sync_runs_end_to_end_and_second_run_is_noop() {
let tmp = TempDir::new().expect("tempdir");
let tmp_path = tmp.path();
let pack_root = tmp_path.join("pack");
copy_dir(&template_root(), &pack_root);
let fake_home = tmp_path.join("home");
fs::create_dir_all(&fake_home).expect("create fake home");
let prev_home = std::env::var("HOME").ok();
let prev_userprofile = std::env::var("USERPROFILE").ok();
std::env::set_var("HOME", &fake_home);
std::env::set_var("USERPROFILE", &fake_home);
let workspace = tmp_path.join("ws");
fs::create_dir_all(&workspace).expect("create ws");
let opts = SyncOptions::new().with_workspace(Some(workspace.clone()));
let cancel = CancellationToken::new();
let report1 = sync::run(&pack_root, &opts, &cancel).expect("first sync ok");
assert!(report1.halted.is_none(), "first sync halted: {:?}", report1.halted);
let performed1 = report1
.steps
.iter()
.filter(|s| matches!(s.exec_step.result, ExecResult::PerformedChange))
.count();
assert!(
performed1 >= 1,
"first sync should perform at least one change (mkdir or symlink); steps: {:?}",
report1.steps
);
let report2 = sync::run(&pack_root, &opts, &cancel).expect("second sync ok");
assert!(report2.halted.is_none(), "second sync halted: {:?}", report2.halted);
let performed2 = report2
.steps
.iter()
.filter(|s| matches!(s.exec_step.result, ExecResult::PerformedChange))
.count();
assert_eq!(
performed2, 0,
"second sync must be an all-no-op (idempotency contract); steps: {:?}",
report2.steps
);
match prev_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match prev_userprofile {
Some(v) => std::env::set_var("USERPROFILE", v),
None => std::env::remove_var("USERPROFILE"),
}
}