mod common;
use common::grex;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use tempfile::TempDir;
fn init_git_identity() {
static ONCE: OnceLock<()> = OnceLock::new();
ONCE.get_or_init(|| {
std::env::set_var("GIT_AUTHOR_NAME", "grex-test");
std::env::set_var("GIT_AUTHOR_EMAIL", "test@grex.local");
std::env::set_var("GIT_COMMITTER_NAME", "grex-test");
std::env::set_var("GIT_COMMITTER_EMAIL", "test@grex.local");
let null_cfg = std::env::temp_dir().join("grex-test-empty-gitconfig");
let _ = fs::write(&null_cfg, b"");
std::env::set_var("GIT_CONFIG_GLOBAL", &null_cfg);
std::env::set_var("GIT_CONFIG_SYSTEM", &null_cfg);
std::env::set_var("GIT_CONFIG_NOSYSTEM", "1");
});
}
fn run_git(cwd: &Path, args: &[&str]) {
let out = Command::new("git").args(args).current_dir(cwd).output().expect("git on PATH");
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
struct Layout {
_tmp: TempDir,
root: PathBuf,
}
fn build_layout(declarative_kids: &[&str], plain_git_kids: &[&str]) -> Layout {
init_git_identity();
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
fs::create_dir_all(root.join(".grex")).unwrap();
let mut parent_yaml =
String::from("schema_version: \"1\"\nname: root\ntype: meta\nchildren:\n");
for name in declarative_kids.iter().chain(plain_git_kids.iter()) {
parent_yaml
.push_str(&format!(" - url: https://example.invalid/{name}.git\n path: {name}\n"));
}
fs::write(root.join(".grex/pack.yaml"), parent_yaml).unwrap();
for name in declarative_kids {
let dir = root.join(name).join(".grex");
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("pack.yaml"),
format!("schema_version: \"1\"\nname: {name}\ntype: declarative\n"),
)
.unwrap();
}
for name in plain_git_kids {
let dir = root.join(name);
fs::create_dir_all(&dir).unwrap();
run_git(&dir, &["init", "-q", "-b", "main"]);
}
Layout { _tmp: tmp, root }
}
#[test]
fn ls_meta_root_and_declarative_child_renders_plain_tree() {
let layout = build_layout(&["warp-cfg"], &[]);
let assertion = grex().current_dir(&layout.root).args(["ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
assert!(stdout.contains("root (meta)"), "stdout must show root meta line; got:\n{stdout}");
assert!(
stdout.contains("warp-cfg (declarative)"),
"stdout must show declarative child; got:\n{stdout}"
);
assert!(
!stdout.contains("~ "),
"no synthetic markers expected for declarative-only tree; got:\n{stdout}"
);
assert!(!stdout.contains("synthetic"), "no `synthetic` text expected; got:\n{stdout}");
}
#[test]
fn ls_plain_git_child_renders_synthetic_marker_in_tree_mode() {
let layout = build_layout(&[], &["algo-leet"]);
let assertion = grex().current_dir(&layout.root).args(["ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
assert!(stdout.contains("root (meta)"), "root meta line missing; got:\n{stdout}");
assert!(
stdout.contains("~ algo-leet (scripted, synthetic)"),
"synthetic plain-git child must render with `~ ` prefix and `scripted, synthetic` suffix; got:\n{stdout}"
);
}
#[test]
fn ls_plain_git_child_renders_synthetic_true_in_json_mode() {
let layout = build_layout(&[], &["algo-leet"]);
let assertion = grex().current_dir(&layout.root).args(["--json", "ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
let v: Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("ls --json invalid JSON: {e}\n{stdout}"));
assert!(v.get("workspace").and_then(Value::as_str).is_some(), "workspace key missing");
let tree = v.get("tree").and_then(Value::as_array).expect("tree must be an array");
assert_eq!(tree.len(), 1, "expected single root entry");
let root = &tree[0];
assert_eq!(root.get("name").and_then(Value::as_str), Some("root"));
assert_eq!(root.get("type").and_then(Value::as_str), Some("meta"));
assert_eq!(root.get("synthetic").and_then(Value::as_bool), Some(false));
let kids = root.get("children").and_then(Value::as_array).expect("children must be an array");
assert_eq!(kids.len(), 1, "expected exactly one child");
let child = &kids[0];
assert_eq!(child.get("name").and_then(Value::as_str), Some("algo-leet"));
assert_eq!(child.get("type").and_then(Value::as_str), Some("scripted"));
assert_eq!(
child.get("synthetic").and_then(Value::as_bool),
Some(true),
"plain-git child must carry synthetic=true"
);
}
#[test]
fn ls_unsynced_children_render_as_placeholders() {
init_git_identity();
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
fs::create_dir_all(root.join(".grex")).unwrap();
fs::write(
root.join(".grex/pack.yaml"),
"schema_version: \"1\"\nname: root\ntype: meta\nchildren:\n - url: https://example.invalid/alpha.git\n path: alpha\n - url: https://example.invalid/beta.git\n path: beta\n",
)
.unwrap();
let assertion = grex().current_dir(&root).args(["ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("alpha (declared, unsynced)"),
"alpha should render with unsynced suffix; got:\n{stdout}"
);
assert!(
stdout.contains("beta (declared, unsynced)"),
"beta should render with unsynced suffix; got:\n{stdout}"
);
let assertion = grex().current_dir(&root).args(["--json", "ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
let v: Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("ls --json invalid JSON: {e}\n{stdout}"));
let kids = v.pointer("/tree/0/children").and_then(Value::as_array).expect("children array");
assert_eq!(kids.len(), 2, "both declared children must appear; got:\n{stdout}");
for kid in kids {
assert_eq!(
kid.get("unsynced").and_then(Value::as_bool),
Some(true),
"kid must carry unsynced=true; got:\n{kid}"
);
}
}
#[test]
fn ls_corrupt_child_yaml_renders_parse_error_marker() {
init_git_identity();
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
fs::create_dir_all(root.join(".grex")).unwrap();
fs::write(
root.join(".grex/pack.yaml"),
"schema_version: \"1\"\nname: root\ntype: meta\nchildren:\n - url: https://example.invalid/corrupt.git\n path: corrupt\n",
)
.unwrap();
fs::create_dir_all(root.join("corrupt/.grex")).unwrap();
fs::write(root.join("corrupt/.grex/pack.yaml"), "::: not yaml ::: : :\n").unwrap();
let assertion = grex().current_dir(&root).args(["ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("corrupt") && stdout.contains("[error: parse]"),
"corrupt child should render with `[error: parse]` indicator; got:\n{stdout}"
);
let assertion = grex().current_dir(&root).args(["--json", "ls"]).assert().success();
let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
let v: Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("ls --json invalid JSON: {e}\n{stdout}"));
let kid = v.pointer("/tree/0/children/0").expect("child entry");
let err = kid.get("error").expect("error envelope present");
assert_eq!(err.get("kind").and_then(Value::as_str), Some("parse"));
assert!(
err.get("message").and_then(Value::as_str).is_some_and(|s| !s.is_empty()),
"error.message must be non-empty"
);
}