use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
fn zacor_bin() -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_BIN_EXE_zacor"));
if !path.exists() {
path = PathBuf::from("target/debug/zacor").with_extension(std::env::consts::EXE_EXTENSION);
}
path
}
fn zr_bin() -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_BIN_EXE_zr"));
if !path.exists() {
path = PathBuf::from("target/debug/zr").with_extension(std::env::consts::EXE_EXTENSION);
}
path
}
fn temp_home() -> TempDir {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("modules")).unwrap();
fs::create_dir_all(dir.path().join("store")).unwrap();
fs::create_dir_all(dir.path().join("cache")).unwrap();
fs::create_dir_all(dir.path().join("cache").join("repos")).unwrap();
fs::create_dir_all(dir.path().join("registries")).unwrap();
dir
}
fn write_receipt(home: &Path, name: &str, version: &str, active: bool) {
let receipt = serde_json::json!({
"schema": 1,
"current": version,
"active": active,
"mode": "command",
"transport": "local",
"config": {},
"versions": {
version: {
"source": { "type": "local", "path": "/tmp/test" },
"installed_at": "2026-03-20T10:30:00Z"
}
}
});
let path = home.join("modules").join(format!("{}.json", name));
fs::write(&path, serde_json::to_string_pretty(&receipt).unwrap()).unwrap();
}
fn write_definition(home: &Path, name: &str, version: &str, yaml: &str) {
let dir = home.join("store").join(name).join(version);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("package.yaml"), yaml).unwrap();
}
#[test]
fn test_definition_only_install() {
let home = temp_home();
let tmp = TempDir::new().unwrap();
let yaml = "name: my-wrapper\nversion: \"1.0.0\"\ncommands:\n default:\n invoke: \"echo hello\"\n description: test\n";
let yaml_path = tmp.path().join("my-wrapper.yaml");
fs::write(&yaml_path, yaml).unwrap();
let output = Command::new(zacor_bin())
.args(["install", &yaml_path.to_string_lossy()])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor install");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"install should succeed: {}",
stderr
);
assert!(stderr.contains("installed my-wrapper"));
let receipt_path = home.path().join("modules").join("my-wrapper.json");
assert!(receipt_path.exists(), "receipt should exist");
let def_path = home.path().join("store").join("my-wrapper").join("1.0.0").join("package.yaml");
assert!(def_path.exists(), "definition should be in store");
let store_dir = home.path().join("store").join("my-wrapper").join("1.0.0");
let files: Vec<_> = fs::read_dir(&store_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name() != "package.yaml")
.collect();
assert!(files.is_empty(), "no binary should exist for definition-only package");
}
#[test]
fn test_version_removal_switches_to_highest() {
let home = temp_home();
let receipt = serde_json::json!({
"schema": 1,
"current": "14.1.0",
"active": true,
"mode": "command",
"transport": "local",
"config": {},
"versions": {
"2.0.0": { "source": { "type": "local", "path": "/tmp/test" }, "installed_at": "2026-01-01T00:00:00Z" },
"13.0.0": { "source": { "type": "local", "path": "/tmp/test" }, "installed_at": "2026-02-01T00:00:00Z" },
"14.1.0": { "source": { "type": "local", "path": "/tmp/test" }, "installed_at": "2026-03-01T00:00:00Z" }
}
});
fs::write(
home.path().join("modules/tool.json"),
serde_json::to_string_pretty(&receipt).unwrap(),
).unwrap();
for v in &["2.0.0", "13.0.0", "14.1.0"] {
write_definition(home.path(), "tool", v, "name: tool\nversion: \"1.0.0\"\ncommands:\n default:\n description: test\n");
}
let output = Command::new(zacor_bin())
.args(["remove", "tool@14.1.0"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor remove");
assert!(output.status.success(), "remove should succeed: {}", String::from_utf8_lossy(&output.stderr));
let receipt_content = fs::read_to_string(home.path().join("modules/tool.json")).unwrap();
let r: serde_json::Value = serde_json::from_str(&receipt_content).unwrap();
assert_eq!(r["current"].as_str().unwrap(), "13.0.0");
}
#[test]
fn test_package_name_validation_in_install() {
let home = temp_home();
let tmp = TempDir::new().unwrap();
let yaml = "name: My_Tool\nversion: \"1.0.0\"\ncommands:\n default:\n description: test\n";
let yaml_path = tmp.path().join("wrapper.yaml");
fs::write(&yaml_path, yaml).unwrap();
let output = Command::new(zacor_bin())
.args(["install", &yaml_path.to_string_lossy()])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor install");
assert!(!output.status.success(), "install should fail for invalid package name");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("invalid") || stderr.contains("must start with a lowercase"), "got: {}", stderr);
}
#[test]
fn test_receipt_forward_compat() {
let home = temp_home();
let receipt = serde_json::json!({
"schema": 99,
"current": "1.0.0",
"active": true,
"mode": "command",
"transport": "local",
"config": {},
"versions": {
"1.0.0": { "source": { "type": "local", "path": "/tmp/test" }, "installed_at": "2026-03-20T10:30:00Z" }
}
});
fs::write(
home.path().join("modules/future-tool.json"),
serde_json::to_string_pretty(&receipt).unwrap(),
).unwrap();
let output = Command::new(zr_bin())
.args(["future-tool"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zr");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(stderr.contains("newer version") || stderr.contains("upgrade"), "got: {}", stderr);
}
#[test]
fn test_no_default_command() {
let home = temp_home();
write_receipt(home.path(), "my-tool", "1.0.0", true);
write_definition(
home.path(),
"my-tool",
"1.0.0",
"name: my-tool\nversion: \"1.0.0\"\ncommands:\n transcribe:\n description: transcribe audio\n translate:\n description: translate audio\n",
);
let output = Command::new(zr_bin())
.args(["my-tool"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zr");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(
stderr.contains("requires a subcommand") || stderr.contains("subcommand"),
"should indicate a subcommand is required, got: {}",
stderr
);
}
#[test]
fn test_corrupt_package_yaml() {
let home = temp_home();
write_receipt(home.path(), "broken", "1.0.0", true);
let output = Command::new(zr_bin())
.args(["broken"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zr");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(
stderr.contains("not found in store") || stderr.contains("reinstall"),
"should suggest reinstall, got: {}",
stderr
);
}
#[test]
fn test_zr_unknown_flag() {
let home = temp_home();
let output = Command::new(zr_bin())
.args(["--unknown-flag"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zr");
assert!(!output.status.success());
}
#[test]
fn test_zr_works_with_nonexistent_home() {
let tmp = TempDir::new().unwrap();
let home = tmp.path().join("nonexistent_zr_home");
let output = Command::new(zr_bin())
.args(["nonexistent-package"])
.env("ZR_HOME", home.to_str().unwrap())
.output()
.expect("failed to run zr");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("not found"), "got: {}", stderr);
}
#[test]
fn test_completions_valid_shells() {
let home = temp_home();
for shell in &["bash", "zsh", "fish", "powershell"] {
let output = Command::new(zacor_bin())
.args(["completions", shell])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor completions");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(output.status.success(), "shell '{}' failed", shell);
assert!(!stdout.is_empty(), "shell '{}' produced no output", shell);
}
}
#[test]
fn test_completions_invalid_shell() {
let home = temp_home();
let output = Command::new(zacor_bin())
.args(["completions", "invalid"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor completions");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unsupported shell"),
"should mention unsupported shell, got: {}",
stderr
);
}
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
fn daemon_request(addr: &str, req: &serde_json::Value) -> serde_json::Value {
let mut stream = TcpStream::connect(addr).expect("failed to connect to daemon");
stream.set_read_timeout(Some(std::time::Duration::from_secs(5))).ok();
let json = serde_json::to_string(req).unwrap();
writeln!(stream, "{}", json).unwrap();
stream.flush().unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).expect("failed to read daemon response");
serde_json::from_str(line.trim()).expect("failed to parse daemon response")
}
#[test]
#[ignore] fn test_daemon_start_ping_status_stop() {
let home = temp_home();
let mut child = Command::new(zacor_bin())
.args(["daemon", "start"])
.env("ZR_HOME", home.path().to_str().unwrap())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn daemon");
let addr = "127.0.0.1:19100";
let start = std::time::Instant::now();
let mut connected = false;
while start.elapsed() < std::time::Duration::from_secs(5) {
if TcpStream::connect(addr).is_ok() {
connected = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(connected, "daemon should start and accept connections");
let resp = daemon_request(addr, &serde_json::json!({"request": "ping"}));
assert_eq!(resp["ok"], true);
let resp = daemon_request(addr, &serde_json::json!({"request": "status"}));
assert_eq!(resp["ok"], true);
assert_eq!(resp["services"].as_array().unwrap().len(), 0);
let resp = daemon_request(addr, &serde_json::json!({"request": "shutdown"}));
assert_eq!(resp["ok"], true);
let status = child.wait().expect("failed to wait for daemon");
assert!(status.success(), "daemon should exit cleanly");
std::thread::sleep(std::time::Duration::from_millis(100));
}
#[test]
fn test_daemon_stop_when_not_running() {
let home = temp_home();
let output = Command::new(zacor_bin())
.args(["daemon", "stop"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor daemon stop");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("not running"), "got: {}", stdout);
}
#[test]
fn test_daemon_status_when_not_running() {
let home = temp_home();
let output = Command::new(zacor_bin())
.args(["daemon", "status"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor daemon status");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("not running"), "got: {}", stdout);
}
fn write_config(home: &Path, toml_content: &str) {
fs::write(home.join("config.toml"), toml_content).unwrap();
}
fn create_mock_registry(home: &Path, registry_name: &str, packages: &[(&str, &str)]) {
let reg_dir = home.join("registries").join(registry_name);
for (pkg_name, index_toml) in packages {
let pkg_dir = reg_dir.join("packages").join(pkg_name);
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("index.toml"), index_toml).unwrap();
}
fs::write(reg_dir.join(".zr-last-sync"), "").unwrap();
}
#[test]
fn test_registry_cli_add_list_remove() {
let home = temp_home();
let output = Command::new(zacor_bin())
.args(["registry", "add", "https://github.com/my-org/zr-packages", "--name", "company"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor registry add");
assert!(output.status.success(), "add should succeed: {}", String::from_utf8_lossy(&output.stderr));
let output = Command::new(zacor_bin())
.args(["registry", "list"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor registry list");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("company"), "list should show company registry, got: {}", stdout);
let output = Command::new(zacor_bin())
.args(["registry", "add", "https://example.com/other", "--name", "company"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor registry add");
assert!(!output.status.success(), "duplicate add should fail");
let output = Command::new(zacor_bin())
.args(["registry", "remove", "company"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor registry remove");
assert!(output.status.success(), "remove should succeed: {}", String::from_utf8_lossy(&output.stderr));
let output = Command::new(zacor_bin())
.args(["registry", "list"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor registry list");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("no registries"), "list should be empty after remove, got: {}", stdout);
let output = Command::new(zacor_bin())
.args(["registry", "remove", "nonexistent"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor registry remove");
assert!(!output.status.success(), "remove nonexistent should fail");
}
#[test]
fn test_install_bare_name_from_mock_registry() {
let home = temp_home();
write_config(home.path(), "[[registries]]\nname = \"test-reg\"\nurl = \"https://example.com/test\"\n");
create_mock_registry(home.path(), "test-reg", &[
("mock-tool", "schema = 1\ndescription = \"A mock tool\"\n\n[[versions]]\nversion = \"1.0.0\"\nrelease = \"nonexistent/mock-tool\"\n"),
]);
let output = Command::new(zacor_bin())
.args(["install", "mock-tool"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor install");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("resolved mock-tool v1.0.0"), "should resolve from registry, got: {}", stderr);
}
#[test]
fn test_install_bare_name_with_version_from_mock_registry() {
let home = temp_home();
write_config(home.path(), "[[registries]]\nname = \"test-reg\"\nurl = \"https://example.com/test\"\n");
create_mock_registry(home.path(), "test-reg", &[
("mock-tool", "schema = 1\n\n[[versions]]\nversion = \"0.1.0\"\nrelease = \"nonexistent/mock-tool\"\n\n[[versions]]\nversion = \"0.2.0\"\nrelease = \"nonexistent/mock-tool\"\n"),
]);
let output = Command::new(zacor_bin())
.args(["install", "mock-tool@0.1.0"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor install");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("resolved mock-tool v0.1.0"), "should resolve v0.1.0, got: {}", stderr);
}
#[test]
#[ignore] fn test_install_git_url() {
let home = temp_home();
let output = Command::new(zacor_bin())
.args(["install", "https://github.com/zacor-packages/p-zr-core.git"])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run zacor install");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "git install should succeed: {}", stderr);
}
#[test]
#[ignore] fn test_monorepo_git_install_reuses_cache() {
let home = temp_home();
let url = "https://github.com/zacor-packages/p-zr-core.git";
let output = Command::new(zacor_bin())
.args(["install", url])
.env("ZR_HOME", home.path().to_str().unwrap())
.output()
.expect("failed to run first install");
assert!(output.status.success(), "first install should succeed: {}", String::from_utf8_lossy(&output.stderr));
let repos_dir = home.path().join("cache").join("repos");
let repo_count = fs::read_dir(&repos_dir)
.map(|d| d.count())
.unwrap_or(0);
assert_eq!(repo_count, 1, "should have exactly one cached repo");
}