use cmdhub_cli::config::OFFICIAL_PUBLIC_KEY;
use cmdhub_cli::config::{load_or_create_config, resolve_config_path};
use cmdhub_cli::db::{init_db, open_db, search_commands};
use cmdhub_cli::runner::{get_command_by_path, run_command};
use cmdhub_shared::RiskLevel;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use ed25519_dalek::{Signer, SigningKey};
use sha2::{Digest, Sha256};
use std::sync::Mutex;
use tempfile::TempDir;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_config_resolution() {
let _guard = ENV_MUTEX.lock().unwrap();
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().to_path_buf();
std::env::set_var("XDG_CONFIG_HOME", &config_dir);
let config = load_or_create_config(None).unwrap();
assert_eq!(config.api_url, "https://api.cmdhub.io/v1");
assert_eq!(config.timeout_seconds, 30);
let expected_path = resolve_config_path(None);
assert!(expected_path.exists());
}
#[test]
fn test_config_env_override() {
let _guard = ENV_MUTEX.lock().unwrap();
let tmp = TempDir::new().unwrap();
let env_config_path = tmp.path().join("env_config.toml");
std::env::set_var("CMDH_CONFIG", &env_config_path);
let result = load_or_create_config(None);
assert!(result.is_err());
let default_config = cmdhub_cli::config::Config::default();
let toml_str = toml::to_string_pretty(&default_config).unwrap();
std::fs::write(&env_config_path, toml_str).unwrap();
let config = load_or_create_config(None).unwrap();
assert_eq!(config.api_url, "https://api.cmdhub.io/v1");
let expected_path = resolve_config_path(None);
assert_eq!(expected_path, env_config_path);
assert!(expected_path.exists());
std::env::remove_var("CMDH_CONFIG");
}
#[test]
fn test_config_custom_path_override() {
let _guard = ENV_MUTEX.lock().unwrap();
let tmp = TempDir::new().unwrap();
let custom_path = tmp.path().join("custom_config.toml");
let result = load_or_create_config(Some(custom_path.clone()));
assert!(result.is_err());
let default_config = cmdhub_cli::config::Config::default();
let toml_str = toml::to_string_pretty(&default_config).unwrap();
std::fs::write(&custom_path, toml_str).unwrap();
let config = load_or_create_config(Some(custom_path.clone())).unwrap();
assert_eq!(config.api_url, "https://api.cmdhub.io/v1");
let expected_path = resolve_config_path(Some(custom_path.clone()));
assert_eq!(expected_path, custom_path);
assert!(expected_path.exists());
}
#[test]
fn test_search_fallback_and_db() {
let _guard = ENV_MUTEX.lock().unwrap();
let tmp = TempDir::new().unwrap();
let data_dir = tmp.path().to_path_buf();
std::env::set_var("XDG_DATA_HOME", &data_dir);
let conn = open_db().unwrap();
init_db(&conn).unwrap();
conn.execute(
"INSERT INTO apps (app_id, name, install_instructions) VALUES (?1, ?2, ?3)",
("org.github.sl", "sl", "{\"brew\": \"brew install sl\"}"),
)
.unwrap();
conn.execute(
"INSERT INTO arguments (cmd_path, app_id, node_name, node_type, description, risk_level, example_template) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
(
"sl.-l",
"org.github.sl",
"-l",
"arg",
"Display a train moving from left to right",
"safe",
"sl -l",
),
).unwrap();
conn.execute(
"INSERT INTO apps_fts (cmd_path, name, capabilities) VALUES (?1, ?2, ?3)",
("sl.-l", "sl", "Display a train moving from left to right"),
)
.unwrap();
let results = search_commands(&conn, "train", None, 5).unwrap();
assert_eq!(results.len(), 1);
let command = &results[0];
assert_eq!(command.cmd_path, "sl.-l");
assert_eq!(command.app_id, "org.github.sl");
assert_eq!(command.name, "sl");
assert_eq!(command.risk_level, RiskLevel::Safe);
assert_eq!(command.example_template, Some("sl -l".to_string()));
assert_eq!(
command.install_instructions.as_ref().unwrap().brew,
Some("brew install sl".to_string())
);
}
#[test]
fn test_safety_gating() {
let _guard = ENV_MUTEX.lock().unwrap();
let tmp = TempDir::new().unwrap();
let data_dir = tmp.path().to_path_buf();
std::env::set_var("XDG_DATA_HOME", &data_dir);
std::env::set_var("CMD_TEST", "1");
let conn = open_db().unwrap();
init_db(&conn).unwrap();
conn.execute(
"INSERT INTO apps (app_id, name, install_instructions) VALUES (?1, ?2, ?3)",
("org.test.echo", "echo", None::<String>),
)
.unwrap();
conn.execute(
"INSERT INTO arguments (cmd_path, app_id, node_name, node_type, description, risk_level, example_template) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
(
"echo.danger",
"org.test.echo",
"danger",
"arg",
"Dangerous echo",
"dangerous",
"echo danger",
),
).unwrap();
let cmd = get_command_by_path(&conn, "echo.danger").unwrap();
assert_eq!(cmd.risk_level, RiskLevel::Dangerous);
let result = run_command(&conn, "echo.danger", &["hello".to_string()], true);
assert!(result.is_ok());
let result = run_command(&conn, "echo.danger", &["hello".to_string()], false);
assert!(result.is_err());
let err_str = format!("{}", result.unwrap_err());
assert!(
err_str.contains("blocked")
|| err_str.contains("read_line")
|| err_str.contains("standard input")
);
}
#[test]
fn test_signature_verification_and_zstd() {
let seed = [42u8; 32];
let signing_key = SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
let pub_key_bytes = verifying_key.to_bytes();
assert_eq!(pub_key_bytes, OFFICIAL_PUBLIC_KEY);
let db_payload = b"SQLite dummy content";
let compressed = zstd::encode_all(&db_payload[..], 3).unwrap();
let mut hasher = Sha256::new();
hasher.update(&compressed);
let hash_result: [u8; 32] = hasher.finalize().into();
let signature = signing_key.sign(&hash_result);
let sig_bytes = signature.to_bytes();
let verifying_key_dec = VerifyingKey::from_bytes(&pub_key_bytes).unwrap();
let sig_dec = Signature::from_slice(&sig_bytes).unwrap();
let verify_res = verifying_key_dec.verify(&hash_result, &sig_dec);
assert!(verify_res.is_ok());
let decompressed = zstd::decode_all(&compressed[..]).unwrap();
assert_eq!(decompressed, db_payload);
}
#[test]
fn test_skills_integration() {
let _guard = ENV_MUTEX.lock().unwrap();
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().to_path_buf();
std::env::set_var("XDG_CONFIG_HOME", &config_dir);
std::env::set_var("XDG_DATA_HOME", &config_dir);
let conn = open_db().unwrap();
init_db(&conn).unwrap();
let skills_dir = config_dir.join("cmdhub").join("skills");
std::fs::create_dir_all(&skills_dir).unwrap();
let contract_custom = cmdhub_shared::AciCommandContract {
app_id: "org.test.custom".to_string(),
name: "custom_cmd".to_string(),
cmd_path: "custom.run".to_string(),
node_type: cmdhub_shared::NodeType::Root,
description: "A completely custom command shortcut loaded from skills".to_string(),
risk_level: RiskLevel::Safe,
example_template: Some("custom_cmd --do-something".to_string()),
install_instructions: None,
};
let json_content = serde_json::to_string(&contract_custom).unwrap();
std::fs::write(skills_dir.join("custom.json"), json_content).unwrap();
let results = search_commands(&conn, "completely", None, 5).unwrap();
assert!(results.is_empty());
let results_all = cmdhub_cli::db::search_all(&conn, "completely", None, 5).unwrap();
assert_eq!(results_all.len(), 1);
assert_eq!(results_all[0].name, "custom_cmd");
assert_eq!(results_all[0].cmd_path, "custom.run");
}
#[test]
fn test_config_override_strict_validation() {
use assert_cmd::Command;
let mut cmd = Command::cargo_bin("cmdh").unwrap();
cmd.arg("--config")
.arg("non_existent_config_abc_123.toml")
.arg("search")
.arg("test");
cmd.assert()
.failure()
.stderr(predicates::str::contains("does not exist"));
}
#[test]
fn test_output_preset_formatting() {
let _guard = ENV_MUTEX.lock().unwrap();
use assert_cmd::Command;
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().to_path_buf();
std::env::set_var("XDG_DATA_HOME", &data_dir);
let conn = open_db().unwrap();
init_db(&conn).unwrap();
conn.execute(
"INSERT INTO apps (app_id, name, install_instructions) VALUES (?1, ?2, ?3)",
("org.github.git", "git", "{\"brew\": \"brew install git\"}"),
)
.unwrap();
conn.execute(
"INSERT INTO arguments (cmd_path, app_id, node_name, node_type, description, risk_level, example_template) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
(
"git",
"org.github.git",
"git",
"root",
"git version control",
"safe",
"example_template",
),
).unwrap();
conn.execute(
"INSERT INTO apps_fts (cmd_path, name, capabilities) VALUES (?1, ?2, ?3)",
("git", "git", "git version control"),
)
.unwrap();
drop(conn);
let mut cmd = Command::cargo_bin("cmdh").unwrap();
cmd.env("XDG_DATA_HOME", &data_dir)
.arg("search")
.arg("git")
.arg("--usage-only");
let assert = cmd.assert().success();
let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(output.contains("cmd_path"));
assert!(output.contains("example_template"));
assert!(!output.contains("risk_level"));
}
#[test]
fn test_init_command_safety_guards() {
use assert_cmd::Command;
let tmp = tempfile::TempDir::new().unwrap();
let config_path = tmp.path().join("cmdhub/config.toml");
std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
std::fs::write(&config_path, "dummy").unwrap();
let mut cmd = Command::cargo_bin("cmdh").unwrap();
cmd.env("XDG_CONFIG_HOME", tmp.path()).arg("init");
cmd.assert().success();
let val = std::fs::read_to_string(&config_path).unwrap();
assert_eq!(val, "dummy");
let mut cmd_force = Command::cargo_bin("cmdh").unwrap();
cmd_force
.env("XDG_CONFIG_HOME", tmp.path())
.arg("init")
.arg("--force");
cmd_force.assert().success();
let val_overwritten = std::fs::read_to_string(&config_path).unwrap();
assert!(val_overwritten.contains("CmdHub configuration file"));
}