#![expect(
clippy::unwrap_used,
reason = "Test code doesn't need production-level error handling"
)]
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_config(dir: &TempDir, vault_name: &str) -> PathBuf {
let config_path = dir.path().join("capsula.toml");
let config_content = format!(
r#"
[vault]
name = "{vault_name}"
[[pre-run.hooks]]
id = "capture-cwd"
"#,
);
fs::write(&config_path, config_content).unwrap();
config_path
}
fn create_test_config_with_post_run(dir: &TempDir, vault_name: &str) -> PathBuf {
let config_path = dir.path().join("capsula.toml");
let config_content = format!(
r#"
[vault]
name = "{vault_name}"
[[pre-run.hooks]]
id = "capture-cwd"
[[post-run.hooks]]
id = "capture-cwd"
"#,
);
fs::write(&config_path, config_content).unwrap();
config_path
}
#[derive(Deserialize)]
struct TestRunMetadata {
name: String,
}
fn find_first_run(vault_dir: &std::path::Path) -> (PathBuf, String) {
let date_dirs: Vec<_> = fs::read_dir(vault_dir)
.unwrap()
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.collect();
for date_dir in date_dirs {
let run_dirs: Vec<_> = fs::read_dir(date_dir.path())
.unwrap()
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.collect();
for run_dir in run_dirs {
let run_path = run_dir.path();
let metadata_path = run_path.join("_capsula").join("metadata.json");
if !metadata_path.exists() {
continue;
}
let content = fs::read_to_string(&metadata_path).unwrap();
let metadata: TestRunMetadata = serde_json::from_str(&content).unwrap();
return (run_path, metadata.name);
}
}
panic!("No run directory found");
}
#[test]
fn test_capsula_run_creates_run_directory() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("hello");
cmd.assert().success();
let vault_dir = temp_dir.path().join(".capsula").join("test-vault");
assert!(vault_dir.exists(), "Vault directory should exist");
let date_dirs: Vec<_> = fs::read_dir(&vault_dir)
.unwrap()
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.collect();
assert!(
!date_dirs.is_empty(),
"Should have at least one date directory"
);
for date_dir in date_dirs {
let run_dirs: Vec<_> = fs::read_dir(date_dir.path())
.unwrap()
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.collect();
if !run_dirs.is_empty() {
let run_dir = run_dirs[0].path();
let capsula_dir = run_dir.join("_capsula");
assert!(capsula_dir.exists(), "_capsula directory should exist");
assert!(
capsula_dir.join("metadata.json").exists(),
"metadata.json should exist"
);
assert!(
capsula_dir.join("pre-run.json").exists(),
"pre-run.json should exist"
);
assert!(
capsula_dir.join("command.json").exists(),
"command.json should exist"
);
return;
}
}
panic!("No run directory found");
}
#[test]
fn test_capsula_run_dir_prints_path() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("hello");
cmd.assert().success();
let vault_dir = temp_dir.path().join(".capsula").join("test-vault");
let (run_dir, run_name) = find_first_run(&vault_dir);
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-dir")
.arg(&run_name);
cmd.assert()
.success()
.stdout(predicate::str::contains(run_dir.to_string_lossy().as_ref()));
}
#[test]
fn test_capsula_list_shows_runs() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("test");
cmd.assert().success();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("list");
cmd.assert()
.success()
.stdout(predicate::str::contains("TIMESTAMP"))
.stdout(predicate::str::contains("NAME"))
.stdout(predicate::str::contains("COMMAND"));
}
#[test]
fn test_capsula_show_displays_run_details() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("hello-show");
cmd.assert().success();
let vault_dir = temp_dir.path().join(".capsula").join("test-vault");
let (_, run_name) = find_first_run(&vault_dir);
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("show")
.arg(&run_name);
cmd.assert()
.success()
.stdout(predicate::str::contains(format!("Run: {run_name}")))
.stdout(predicate::str::contains("ID:"))
.stdout(predicate::str::contains("Timestamp:"))
.stdout(predicate::str::contains("Result: exit 0"))
.stdout(predicate::str::contains("Pre-run hooks:"))
.stdout(predicate::str::contains("[ok] capture-cwd"));
}
#[test]
fn test_capsula_show_json_output() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("hello-json");
cmd.assert().success();
let vault_dir = temp_dir.path().join(".capsula").join("test-vault");
let (_, run_name) = find_first_run(&vault_dir);
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("show")
.arg(&run_name)
.arg("--json");
let output = cmd.assert().success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.get("metadata").is_some(), "should have metadata key");
assert!(parsed.get("pre_run").is_some(), "should have pre_run key");
assert!(parsed.get("command").is_some(), "should have command key");
assert_eq!(
parsed["metadata"]["name"].as_str().unwrap(),
run_name,
"metadata.name should match run name"
);
assert_eq!(
parsed["command"]["exit_code"].as_i64().unwrap(),
0,
"exit code should be 0"
);
}
#[test]
fn test_capsula_push_requires_server_url() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("test");
cmd.assert().success();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("push")
.arg("01234567890123456789012345");
cmd.assert()
.failure()
.stderr(predicate::str::contains("Server URL not specified"));
}
#[test]
fn test_capsula_vaults_list_requires_server_url() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("vaults")
.arg("list");
cmd.assert()
.failure()
.stderr(predicate::str::contains("Server URL not specified"));
}
#[test]
fn test_capsula_run_with_nonexistent_config() {
let temp_dir = TempDir::new().unwrap();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg("nonexistent.toml")
.arg("run")
.arg("echo")
.arg("test");
cmd.assert()
.failure()
.stderr(predicate::str::contains("Configuration file not found"));
}
#[test]
fn test_capsula_run_without_command() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run");
cmd.assert()
.failure()
.stderr(predicate::str::contains("No command specified"));
}
#[test]
fn test_capsula_config_with_server_url() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("capsula.toml");
let config_content = r#"
[vault]
name = "test-vault"
server = "http://localhost:8500"
[[pre-run.hooks]]
id = "capture-cwd"
"#;
fs::write(&config_path, config_content).unwrap();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run")
.arg("echo")
.arg("test");
cmd.assert().success();
}
#[test]
fn test_run_start_creates_directory_and_pre_run() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config_with_post_run(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-start");
let output = cmd.assert().success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let run_name = stdout.trim();
assert!(
!run_name.is_empty(),
"run-start should print run name to stdout"
);
let vault_dir = temp_dir.path().join(".capsula").join("test-vault");
assert!(vault_dir.exists(), "Vault directory should exist");
let (run_dir, found_name) = find_first_run(&vault_dir);
assert_eq!(found_name, run_name);
let capsula_dir = run_dir.join("_capsula");
assert!(
capsula_dir.join("metadata.json").exists(),
"metadata.json should exist"
);
assert!(
capsula_dir.join("pre-run.json").exists(),
"pre-run.json should exist"
);
assert!(
!capsula_dir.join("command.json").exists(),
"command.json should NOT exist"
);
assert!(
!capsula_dir.join("post-run.json").exists(),
"post-run.json should NOT exist"
);
}
#[test]
fn test_run_start_prints_name_to_stdout() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-start");
let output = cmd.assert().success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(
lines.len(),
1,
"stdout should contain exactly one line (the run name)"
);
assert!(
lines[0].contains('-'),
"Run name should be hyphenated (e.g., 'happy-river')"
);
}
#[test]
fn test_run_end_creates_post_run() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config_with_post_run(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-start");
let output = cmd.assert().success();
let run_name = String::from_utf8(output.get_output().stdout.clone())
.unwrap()
.trim()
.to_string();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-end")
.arg(&run_name);
cmd.assert().success();
let vault_dir = temp_dir.path().join(".capsula").join("test-vault");
let (run_dir, _) = find_first_run(&vault_dir);
let capsula_dir = run_dir.join("_capsula");
assert!(
capsula_dir.join("post-run.json").exists(),
"post-run.json should exist after run-end"
);
}
#[test]
fn test_run_end_nonexistent_name_fails() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-end")
.arg("nonexistent-run");
cmd.assert().failure();
}
#[test]
fn test_run_end_already_finalized_fails() {
let temp_dir = TempDir::new().unwrap();
let config_path = create_test_config_with_post_run(&temp_dir, "test-vault");
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-start");
let output = cmd.assert().success();
let run_name = String::from_utf8(output.get_output().stdout.clone())
.unwrap()
.trim()
.to_string();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-end")
.arg(&run_name);
cmd.assert().success();
let mut cmd = cargo_bin_cmd!("capsula");
cmd.current_dir(temp_dir.path())
.arg("--config")
.arg(&config_path)
.arg("run-end")
.arg(&run_name);
cmd.assert()
.failure()
.stderr(predicate::str::contains("already been finalized"));
}