use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
fn skim_init_cmd(config_dir: &std::path::Path) -> Command {
let mut cmd = Command::cargo_bin("skim").unwrap();
cmd.arg("init")
.env("CLAUDE_CONFIG_DIR", config_dir.as_os_str());
cmd
}
fn skim_rewrite_hook_cmd(config_dir: &std::path::Path) -> Command {
let mut cmd = Command::cargo_bin("skim").unwrap();
cmd.args(["rewrite", "--hook"])
.env("CLAUDE_CONFIG_DIR", config_dir.as_os_str());
cmd
}
#[test]
fn test_install_creates_sha256_file() {
let dir = TempDir::new().unwrap();
let config = dir.path();
skim_init_cmd(config).args(["--yes"]).assert().success();
let manifest_path = config.join("hooks/skim-claude-code.sha256");
assert!(
manifest_path.exists(),
"SHA-256 manifest should be created on install"
);
let content = fs::read_to_string(&manifest_path).unwrap();
assert!(
content.starts_with("sha256:"),
"Manifest should start with sha256: prefix, got: {content}"
);
assert!(
content.contains("skim-rewrite.sh"),
"Manifest should reference the script name, got: {content}"
);
let hash = content
.strip_prefix("sha256:")
.unwrap()
.split_whitespace()
.next()
.unwrap();
assert_eq!(hash.len(), 64, "SHA-256 hash should be 64 hex chars");
assert!(
hash.chars().all(|c| c.is_ascii_hexdigit()),
"Hash should be valid hex"
);
}
#[test]
fn test_upgrade_recomputes_hash() {
let dir = TempDir::new().unwrap();
let config = dir.path();
skim_init_cmd(config).args(["--yes"]).assert().success();
let manifest_path = config.join("hooks/skim-claude-code.sha256");
let _hash1 = fs::read_to_string(&manifest_path).unwrap();
let script_path = config.join("hooks/skim-rewrite.sh");
let content = fs::read_to_string(&script_path).unwrap();
let modified = content.replace("skim-hook v", "skim-hook v0.0.0-old-");
fs::write(&script_path, &modified).unwrap();
skim_init_cmd(config).args(["--yes"]).assert().success();
let hash2 = fs::read_to_string(&manifest_path).unwrap();
assert!(
hash2.starts_with("sha256:"),
"After upgrade, manifest should still be valid"
);
}
#[test]
fn test_uninstall_tampered_requires_force() {
let dir = TempDir::new().unwrap();
let config = dir.path();
skim_init_cmd(config).args(["--yes"]).assert().success();
let script_path = config.join("hooks/skim-rewrite.sh");
fs::write(&script_path, "#!/bin/bash\necho 'tampered'\n").unwrap();
let perms = std::fs::Permissions::from_mode(0o755);
fs::set_permissions(&script_path, perms).unwrap();
skim_init_cmd(config)
.args(["--uninstall", "--yes"])
.assert()
.failure()
.stderr(predicate::str::contains("modified since installation"))
.stderr(predicate::str::contains("--force"));
}
#[test]
fn test_uninstall_with_force_bypasses_warning() {
let dir = TempDir::new().unwrap();
let config = dir.path();
skim_init_cmd(config).args(["--yes"]).assert().success();
let script_path = config.join("hooks/skim-rewrite.sh");
fs::write(&script_path, "#!/bin/bash\necho 'tampered'\n").unwrap();
let perms = std::fs::Permissions::from_mode(0o755);
fs::set_permissions(&script_path, perms).unwrap();
skim_init_cmd(config)
.args(["--uninstall", "--yes", "--force"])
.assert()
.success()
.stderr(predicate::str::contains("proceeding with --force"));
assert!(
!script_path.exists(),
"Hook script should be deleted after forced uninstall"
);
let manifest_path = config.join("hooks/skim-claude-code.sha256");
assert!(
!manifest_path.exists(),
"Hash manifest should be cleaned up after uninstall"
);
}
#[test]
fn test_uninstall_clean_script_proceeds() {
let dir = TempDir::new().unwrap();
let config = dir.path();
skim_init_cmd(config).args(["--yes"]).assert().success();
skim_init_cmd(config)
.args(["--uninstall", "--yes"])
.assert()
.success();
let script_path = config.join("hooks/skim-rewrite.sh");
assert!(!script_path.exists(), "Script should be deleted");
let manifest_path = config.join("hooks/skim-claude-code.sha256");
assert!(!manifest_path.exists(), "Manifest should be deleted");
}
#[test]
fn test_hook_mode_tamper_warning_goes_to_log_not_stderr() {
let dir = TempDir::new().unwrap();
let config = dir.path();
let cache_dir = TempDir::new().unwrap();
skim_init_cmd(config).args(["--yes"]).assert().success();
let script_path = config.join("hooks/skim-rewrite.sh");
fs::write(&script_path, "#!/bin/bash\necho 'tampered'\n").unwrap();
let hook_input = serde_json::json!({
"tool_input": {
"command": "cargo test"
}
});
skim_rewrite_hook_cmd(config)
.env("SKIM_CACHE_DIR", cache_dir.path().as_os_str())
.write_stdin(hook_input.to_string())
.assert()
.success()
.stderr(predicate::str::contains("tampered").not());
let log_path = cache_dir.path().join("hook.log");
assert!(
log_path.exists(),
"Hook log file should exist at {}",
log_path.display()
);
let log_content = fs::read_to_string(&log_path).unwrap();
assert!(
log_content.contains("tampered"),
"Hook log should contain tamper warning, got: {log_content}"
);
}
#[test]
fn test_cleanup_removes_sha256() {
let dir = TempDir::new().unwrap();
let config = dir.path();
skim_init_cmd(config).args(["--yes"]).assert().success();
let manifest_path = config.join("hooks/skim-claude-code.sha256");
assert!(
manifest_path.exists(),
"Manifest should exist after install"
);
skim_init_cmd(config)
.args(["--uninstall", "--yes"])
.assert()
.success();
assert!(
!manifest_path.exists(),
"Manifest should be removed after uninstall"
);
}
#[test]
fn test_integrity_suppresses_version_mismatch() {
let dir = TempDir::new().unwrap();
let config = dir.path();
let cache_dir = TempDir::new().unwrap();
skim_init_cmd(config).args(["--yes"]).assert().success();
let script_path = config.join("hooks/skim-rewrite.sh");
fs::write(&script_path, "#!/bin/bash\necho 'tampered'\n").unwrap();
let hook_input = serde_json::json!({
"tool_input": {
"command": "cargo test"
}
});
skim_rewrite_hook_cmd(config)
.env("SKIM_HOOK_VERSION", "0.0.0-fake")
.env("SKIM_CACHE_DIR", cache_dir.path().as_os_str())
.write_stdin(hook_input.to_string())
.assert()
.success()
.stderr(predicate::str::contains("version mismatch").not());
}