use assert_cmd::Command;
use assert_fs::prelude::*;
use assert_fs::TempDir;
use predicates::prelude::*;
#[test]
fn test_version_command() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.arg("--version");
cmd.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
Ok(())
}
#[test]
fn test_rollback_list_backups() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let backup_dir = temp.child(".local/share/geist-supervisor/backups");
backup_dir.create_dir_all()?;
let backup1 = backup_dir.child("geist_supervisor.backup.v1.0.0.1700000000");
let backup2 = backup_dir.child("geist_supervisor.backup.v1.1.0.1700000100");
backup1.touch()?;
backup2.touch()?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", temp.path());
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("rollback");
cmd.assert().success().stdout(
predicate::str::contains("geist_supervisor.backup.v1.0.0.1700000000").and(
predicate::str::contains("geist_supervisor.backup.v1.1.0.1700000100"),
),
);
Ok(())
}
#[test]
fn test_check_update_json_offline() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let config_dir = temp.child(".local/share/geist-supervisor");
config_dir.create_dir_all()?;
let _config_file = config_dir.child("config.json");
let config_path = temp.child(".local/share/geist-supervisor/current_version");
config_path.write_str("v1.0.0")?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", temp.path()); cmd.env("RUST_LOG", "off"); cmd.env("GEIST_REGISTRY_URL_TEST", "http://127.0.0.1:1"); cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("check-update").arg("--json");
cmd.assert().success().stdout(
predicate::str::contains(r#""update_available":false"#)
.and(predicate::str::contains(r#""current_version":"v1.0.0""#))
.and(predicate::str::contains(r#""error":"#)), );
Ok(())
}
#[test]
fn test_check_update_json_stdout_clean_with_rust_log_info() -> Result<(), Box<dyn std::error::Error>>
{
let temp = TempDir::new()?;
let config_dir = temp.child(".local/share/geist-supervisor");
config_dir.create_dir_all()?;
temp.child(".local/share/geist-supervisor/current_version")
.write_str("v1.0.0")?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", temp.path());
cmd.env("RUST_LOG", "info"); cmd.env("GEIST_REGISTRY_URL_TEST", "http://127.0.0.1:1");
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("check-update").arg("--json");
let output = cmd.output()?;
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(stdout.trim());
assert!(
parsed.is_ok(),
"stdout must be parseable as JSON, got: {stdout}"
);
assert!(
!stdout.contains("INFO"),
"Tracing INFO lines must not appear on stdout: {stdout}"
);
assert!(
stderr.contains("INFO") || stderr.is_empty(),
"Tracing output should go to stderr"
);
Ok(())
}
#[test]
fn test_status_command() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("status");
cmd.assert()
.success()
.stdout(predicate::str::contains("Service status: running"));
Ok(())
}
#[test]
fn test_update_from_local_bundle() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let app_dir = temp.child("app");
app_dir.create_dir_all()?;
let app_binary_path = app_dir.child("geist_supervisor");
let home_dir = temp.child("home");
home_dir.create_dir_all()?;
let bundle_src_dir = temp.child("bundle_src");
bundle_src_dir.create_dir_all()?;
let new_binary_content = b"#!/bin/sh\necho 'new binary v99'";
let binary_in_bundle = bundle_src_dir.child("geist_supervisor");
binary_in_bundle.write_binary(new_binary_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&binary_in_bundle)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&binary_in_bundle, perms)?;
}
let install_script_content = format!(
"#!/bin/sh\n/bin/cp -f ./geist_supervisor {}",
app_binary_path.path().to_str().unwrap()
);
let install_script = bundle_src_dir.child("install.sh");
install_script.write_str(&install_script_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&install_script)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&install_script, perms)?;
}
let metadata_content = r#"{"version": "v99.9.9"}"#;
bundle_src_dir
.child("ota_metadata.json")
.write_str(metadata_content)?;
let bundle_archive_path = temp.child("test_bundle.tar.gz");
let tar_gz = std::fs::File::create(&bundle_archive_path)?;
let enc = flate2::write::GzEncoder::new(tar_gz, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
tar.append_dir_all(".", bundle_src_dir.path())?;
let enc = tar.into_inner()?;
enc.finish()?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("GEIST_APP_BINARY_PATH_TEST", app_binary_path.path());
cmd.env("GEIST_SKIP_DEPENDENCY_CHECK", "1"); cmd.env("HOME", home_dir.path());
cmd.arg("update").arg(bundle_archive_path.path());
cmd.assert()
.success()
.stdout(predicate::str::contains("Update completed successfully!"));
let installed_content = std::fs::read(app_binary_path.path())?;
assert_eq!(installed_content, new_binary_content);
Ok(())
}
#[test]
fn test_update_dry_run_with_local_bundle() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let home_dir = temp.child("home");
home_dir.create_dir_all()?;
let config_dir = home_dir.child(".local/share/geist-supervisor");
config_dir.create_dir_all()?;
let version_file = config_dir.child("current_version");
version_file.write_str("v1.0.0")?;
let bundle_src_dir = temp.child("bundle_src");
bundle_src_dir.create_dir_all()?;
let binary_in_bundle = bundle_src_dir.child("geist_supervisor");
binary_in_bundle.write_binary(b"#!/bin/sh\necho 'new binary v2.0.0'")?;
let metadata_content = r#"{"version": "v2.0.0"}"#;
bundle_src_dir
.child("ota_metadata.json")
.write_str(metadata_content)?;
let install_script = bundle_src_dir.child("install.sh");
install_script.write_str("#!/bin/sh\necho 'install script'")?;
let bundle_archive_path = temp.child("test_bundle.tar.gz");
let tar_gz = std::fs::File::create(&bundle_archive_path)?;
let enc = flate2::write::GzEncoder::new(tar_gz, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
tar.append_dir_all(".", bundle_src_dir.path())?;
let enc = tar.into_inner()?;
enc.finish()?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", home_dir.path());
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("update")
.arg("--dry-run")
.arg(bundle_archive_path.path());
cmd.assert()
.success()
.stdout(predicate::str::contains("=== DRY RUN MODE ==="))
.stdout(predicate::str::contains("Current version: v1.0.0"))
.stdout(predicate::str::contains("Target version: v2.0.0"))
.stdout(predicate::str::contains(
"Would update from v1.0.0 to v2.0.0",
))
.stdout(predicate::str::contains("✓ install.sh script found"))
.stdout(predicate::str::contains("✓ ota_metadata.json found"))
.stdout(predicate::str::contains("Execute install.sh script"))
.stdout(predicate::str::contains(
"No changes were made to the system",
));
let version_content = std::fs::read_to_string(version_file.path())?;
assert_eq!(version_content, "v1.0.0");
Ok(())
}
#[test]
fn test_update_dry_run_same_version() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let home_dir = temp.child("home");
home_dir.create_dir_all()?;
let config_dir = home_dir.child(".local/share/geist-supervisor");
config_dir.create_dir_all()?;
let version_file = config_dir.child("current_version");
version_file.write_str("v1.0.0")?;
let bundle_src_dir = temp.child("bundle_src");
bundle_src_dir.create_dir_all()?;
let metadata_content = r#"{"version": "v1.0.0"}"#;
bundle_src_dir
.child("ota_metadata.json")
.write_str(metadata_content)?;
let binary_in_bundle = bundle_src_dir.child("geist_supervisor");
binary_in_bundle.write_binary(b"#!/bin/sh\necho 'same version'")?;
let bundle_archive_path = temp.child("test_bundle.tar.gz");
let tar_gz = std::fs::File::create(&bundle_archive_path)?;
let enc = flate2::write::GzEncoder::new(tar_gz, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
tar.append_dir_all(".", bundle_src_dir.path())?;
let enc = tar.into_inner()?;
enc.finish()?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", home_dir.path());
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("update")
.arg("--dry-run")
.arg(bundle_archive_path.path());
cmd.assert()
.success()
.stdout(predicate::str::contains("=== DRY RUN MODE ==="))
.stdout(predicate::str::contains(
"Already on target version v1.0.0, no update needed",
));
Ok(())
}
#[test]
fn test_update_dry_run_remote_latest() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let home_dir = temp.child("home");
home_dir.create_dir_all()?;
let config_dir = home_dir.child(".local/share/geist-supervisor");
config_dir.create_dir_all()?;
let version_file = config_dir.child("current_version");
version_file.write_str("v1.0.0")?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", home_dir.path());
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.env("GEIST_REGISTRY_URL_TEST", "http://127.0.0.1:1"); cmd.arg("update").arg("--dry-run").arg("latest");
cmd.assert()
.success()
.stdout(predicate::str::contains("=== DRY RUN MODE ==="))
.stdout(predicate::str::contains("Current version: v1.0.0"))
.stdout(predicate::str::contains(
"Error: Failed to fetch latest version",
));
Ok(())
}
#[test]
fn test_update_dry_run_without_install_script() -> Result<(), Box<dyn std::error::Error>> {
let temp = TempDir::new()?;
let home_dir = temp.child("home");
home_dir.create_dir_all()?;
let config_dir = home_dir.child(".local/share/geist-supervisor");
config_dir.create_dir_all()?;
let version_file = config_dir.child("current_version");
version_file.write_str("v1.0.0")?;
let bundle_src_dir = temp.child("bundle_src");
bundle_src_dir.create_dir_all()?;
let binary_in_bundle = bundle_src_dir.child("geist_supervisor");
binary_in_bundle.write_binary(b"#!/bin/sh\necho 'binary only'")?;
let metadata_content = r#"{"version": "v2.0.0"}"#;
bundle_src_dir
.child("ota_metadata.json")
.write_str(metadata_content)?;
let bundle_archive_path = temp.child("test_bundle.tar.gz");
let tar_gz = std::fs::File::create(&bundle_archive_path)?;
let enc = flate2::write::GzEncoder::new(tar_gz, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
tar.append_dir_all(".", bundle_src_dir.path())?;
let enc = tar.into_inner()?;
enc.finish()?;
let mut cmd = Command::new(env!("CARGO_BIN_EXE_geist_supervisor"));
cmd.env("HOME", home_dir.path());
cmd.env("GEIST_APP_BINARY_PATH_TEST", "/tmp/test");
cmd.arg("update")
.arg("--dry-run")
.arg(bundle_archive_path.path());
cmd.assert()
.success()
.stdout(predicate::str::contains(
"✗ No install.sh script - binary-only update mode",
))
.stdout(predicate::str::contains("5. Replace binary only"));
Ok(())
}