use assert_cmd::Command;
use predicates::str::contains;
use std::ffi::OsString;
use std::fs;
use tempfile::TempDir;
fn path_with_fake_kimi(tmp: &TempDir) -> OsString {
let bin_dir = tmp.path().join("bin");
fs::create_dir_all(&bin_dir).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let kimi = bin_dir.join("kimi");
fs::write(&kimi, "#!/bin/sh\necho 'kimi 0.0.0-test'\n").unwrap();
fs::set_permissions(&kimi, fs::Permissions::from_mode(0o755)).unwrap();
}
#[cfg(windows)]
{
fs::write(
bin_dir.join("kimi.cmd"),
"@echo off\r\necho kimi 0.0.0-test\r\n",
)
.unwrap();
}
let mut paths = vec![bin_dir];
paths.extend(std::env::split_paths(
&std::env::var_os("PATH").unwrap_or_default(),
));
std::env::join_paths(paths).unwrap()
}
#[test]
fn test_kimi_native_help() {
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.arg("kimi").arg("--help");
cmd.assert()
.success()
.stdout(contains("sync"))
.stdout(contains("doctor"))
.stdout(contains("install"))
.stdout(contains("rollback"))
.stdout(contains("agents"))
.stdout(contains("hooks"))
.stdout(contains("skills"));
}
#[test]
fn test_kimi_native_alias_k() {
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.arg("k").arg("--help");
cmd.assert().success().stdout(contains("sync"));
}
#[test]
fn test_kimi_sync_creates_agents_and_hooks() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("sync");
cmd.assert().success().stdout(contains("Sync complete"));
let agents_dir = tmp.path().join(".kimi").join("agents");
assert!(agents_dir.join("architect").join("agent.yaml").exists());
assert!(agents_dir.join("architect").join("system.md").exists());
assert!(agents_dir.join("executor").join("agent.yaml").exists());
let hooks_dir = tmp.path().join(".kimi").join("hooks");
assert!(hooks_dir.join("safety-check.sh").exists());
assert!(hooks_dir.join("completion-check.sh").exists());
assert!(hooks_dir.join("notify.sh").exists());
}
#[test]
fn test_kimi_sync_creates_manifest() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("sync");
cmd.assert().success();
let manifest = tmp.path().join(".kimi").join("omk-manifest.json");
assert!(manifest.exists());
let content = fs::read_to_string(&manifest).unwrap();
assert!(content.contains("agent_spec"));
assert!(content.contains("agent_prompt"));
assert!(content.contains("hook_script"));
}
#[test]
fn test_kimi_doctor_reports_missing() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert()
.success()
.stdout(contains("Kimi-native doctor"))
.stdout(contains("issue(s) found"));
}
#[test]
fn test_kimi_doctor_passes_after_sync() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
fs::write(tmp.path().join("AGENTS.md"), "# Test\n").unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.env("PATH", path_with_fake_kimi(&tmp));
cmd.arg("kimi").arg("doctor");
cmd.assert().success().stdout(contains("All checks passed"));
}
#[test]
fn test_kimi_doctor_detects_drift() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
fs::remove_file(tmp.path().join(".kimi/agents/architect/agent.yaml")).unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert()
.success()
.stdout(contains("Missing manifest file"));
}
#[test]
fn test_kimi_doctor_reports_backup_index_drift_with_repair_command() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hook_path = tmp.path().join(".kimi/hooks/safety-check.sh");
fs::write(&hook_path, "# user-changed\n").unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hooks_dir = tmp.path().join(".kimi/hooks");
let backup = fs::read_dir(&hooks_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| {
p.file_name()
.and_then(|name| name.to_str())
.map(|name| name.contains(".omk-backup-"))
.unwrap_or(false)
})
.expect("expected backup file");
fs::remove_file(&backup).unwrap();
let mut doctor_cmd = Command::cargo_bin("omk").unwrap();
doctor_cmd.current_dir(&tmp);
doctor_cmd.arg("kimi").arg("doctor");
doctor_cmd
.assert()
.success()
.stdout(contains("Backup index drift"))
.stdout(contains("omk kimi sync --force"));
}
#[test]
fn test_kimi_agents_lists_roles() {
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.arg("kimi").arg("agents");
cmd.assert()
.success()
.stdout(contains("architect"))
.stdout(contains("executor"))
.stdout(contains("verifier"));
}
#[test]
fn test_kimi_hooks_lists_events() {
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.arg("kimi").arg("hooks");
cmd.assert()
.success()
.stdout(contains("PreToolUse"))
.stdout(contains("safety-check.sh"));
}
#[test]
fn test_kimi_install_creates_assets() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("install");
cmd.assert()
.success()
.stdout(contains("Installation complete"));
assert!(tmp.path().join(".kimi").join("agents").exists());
assert!(tmp.path().join(".kimi").join("hooks").exists());
assert!(tmp.path().join(".kimi").join("hooks.toml.example").exists());
}
#[test]
fn test_kimi_rollback_removes_assets() {
let tmp = TempDir::new().unwrap();
let mut install_cmd = Command::cargo_bin("omk").unwrap();
install_cmd.current_dir(&tmp);
install_cmd.arg("kimi").arg("install");
install_cmd.assert().success();
assert!(tmp
.path()
.join(".kimi/agents/architect/agent.yaml")
.exists());
let mut rollback_cmd = Command::cargo_bin("omk").unwrap();
rollback_cmd.current_dir(&tmp);
rollback_cmd.arg("kimi").arg("rollback");
rollback_cmd
.assert()
.success()
.stdout(contains("Rollback complete"));
assert!(!tmp
.path()
.join(".kimi/agents/architect/agent.yaml")
.exists());
assert!(!tmp.path().join(".kimi/omk-manifest.json").exists());
}
#[test]
fn test_kimi_sync_creates_backup_on_overwrite() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hook_path = tmp.path().join(".kimi/hooks/safety-check.sh");
let original = fs::read_to_string(&hook_path).unwrap();
fs::write(&hook_path, "# modified\n").unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hooks_dir = tmp.path().join(".kimi/hooks");
let backups: Vec<_> = fs::read_dir(&hooks_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".omk-backup-"))
.collect();
assert!(!backups.is_empty(), "Expected backup file to be created");
let backup_path = backups[0].path();
let backup_content = fs::read_to_string(&backup_path).unwrap();
assert_eq!(backup_content, "# modified\n");
let restored_content = fs::read_to_string(&hook_path).unwrap();
assert_eq!(restored_content, original);
}
#[test]
fn test_kimi_sync_records_backup_index_in_manifest() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hook_path = tmp.path().join(".kimi/hooks/safety-check.sh");
fs::write(&hook_path, "# modified for backup index\n").unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let manifest_path = tmp.path().join(".kimi/omk-manifest.json");
let manifest = fs::read_to_string(&manifest_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&manifest).unwrap();
let backups = json["backups"]
.as_array()
.expect("manifest must contain backups array");
assert!(
!backups.is_empty(),
"expected at least one backup mapping in manifest"
);
let linked = backups.iter().any(|entry| {
entry["managed_path"].as_str() == Some(".kimi/hooks/safety-check.sh")
&& entry["backup_path"]
.as_str()
.map(|p| p.contains(".omk-backup-"))
.unwrap_or(false)
});
assert!(
linked,
"backup index must link managed path to backup artifact"
);
}
#[test]
fn test_kimi_rollback_no_manifest() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("rollback");
cmd.assert()
.success()
.stdout(contains("Nothing to rollback"));
}
#[test]
fn test_kimi_sync_dry_run_no_files_written() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("sync").arg("--dry-run");
cmd.assert().success().stdout(contains("Dry run"));
assert!(!tmp.path().join(".kimi").exists());
}
#[test]
fn test_kimi_sync_dry_run_explicit_project_and_user_scopes() {
let tmp = TempDir::new().unwrap();
let xdg_config = tmp.path().join("xdg-config");
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.env("XDG_CONFIG_HOME", &xdg_config);
cmd.arg("kimi").arg("sync").arg("--dry-run");
cmd.assert()
.success()
.stdout(contains("Project-level would"))
.stdout(contains("User-level summary"));
}
#[test]
fn test_kimi_rollback_dry_run_no_files_deleted() {
let tmp = TempDir::new().unwrap();
let mut install_cmd = Command::cargo_bin("omk").unwrap();
install_cmd.current_dir(&tmp);
install_cmd.arg("kimi").arg("install");
install_cmd.assert().success();
assert!(tmp
.path()
.join(".kimi/agents/architect/agent.yaml")
.exists());
let mut rollback_cmd = Command::cargo_bin("omk").unwrap();
rollback_cmd.current_dir(&tmp);
rollback_cmd.arg("kimi").arg("rollback").arg("--dry-run");
rollback_cmd
.assert()
.success()
.stdout(contains("Dry run"))
.stdout(contains("Skipped:"))
.stdout(contains("Report: restored="))
.stdout(contains("errors=0"));
assert!(tmp
.path()
.join(".kimi/agents/architect/agent.yaml")
.exists());
assert!(tmp.path().join(".kimi/omk-manifest.json").exists());
}
#[test]
fn test_kimi_rollback_restores_backup() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hook_path = tmp.path().join(".kimi/hooks/safety-check.sh");
fs::write(&hook_path, "# modified by user\n").unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hooks_dir = tmp.path().join(".kimi/hooks");
let backups: Vec<_> = fs::read_dir(&hooks_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".omk-backup-"))
.collect();
assert!(!backups.is_empty(), "Expected backup file to be created");
let mut rollback_cmd = Command::cargo_bin("omk").unwrap();
rollback_cmd.current_dir(&tmp);
rollback_cmd.arg("kimi").arg("rollback");
rollback_cmd
.assert()
.success()
.stdout(contains("Restored from backup"));
let content = fs::read_to_string(&hook_path).unwrap();
assert_eq!(content, "# modified by user\n");
}
#[test]
fn test_kimi_doctor_json_output() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor").arg("--json");
cmd.assert()
.success()
.stdout(contains("\"severity\""))
.stdout(contains("\"message\""));
}
#[test]
fn test_kimi_doctor_missing_kimi_prints_repair_command() {
let tmp = TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.env("PATH", "");
cmd.arg("kimi").arg("doctor");
cmd.assert()
.success()
.stdout(contains("Kimi CLI not found in PATH"))
.stdout(contains("command -v kimi && kimi --version"));
}
#[test]
fn test_kimi_doctor_rejects_manifest_path_escape() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let manifest = tmp.path().join(".kimi/omk-manifest.json");
let content = fs::read_to_string(&manifest).unwrap();
let corrupted = content.replacen(".kimi/agents/architect/agent.yaml", "../../escape.txt", 1);
fs::write(&manifest, corrupted).unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert()
.success()
.stdout(contains("Cannot read asset manifest"))
.stdout(contains("escapes allowed roots"));
}
#[test]
fn test_kimi_sync_skips_identical_files() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success().stdout(contains("unchanged"));
let hooks_dir = tmp.path().join(".kimi/hooks");
let backups: Vec<_> = fs::read_dir(&hooks_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".omk-backup-"))
.collect();
assert!(
backups.is_empty(),
"Expected no backups for identical files"
);
}
#[test]
fn test_kimi_doctor_detects_invalid_agent() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let agent_yaml = tmp.path().join(".kimi/agents/architect/agent.yaml");
fs::write(&agent_yaml, "not: valid yaml {{").unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert().success().stdout(contains("invalid YAML"));
}
#[test]
fn test_kimi_doctor_detects_dangling_hook_config() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let hooks_toml = tmp.path().join(".kimi/hooks.toml.example");
fs::write(
&hooks_toml,
r#"[[hooks]]
event = "PreToolUse"
command = ".kimi/hooks/missing.sh"
"#,
)
.unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert().success().stdout(contains("missing scripts"));
}
#[test]
fn test_kimi_doctor_validates_skills_paths() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
let skill_dir = tmp.path().join(".kimi/skills/test-skill");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(skill_dir.join("SKILL.md"), "# Test skill\n").unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert()
.success()
.stdout(contains("Skills paths valid (1 skill(s))"));
}
#[test]
fn test_kimi_doctor_detects_invalid_skills_path() {
let tmp = TempDir::new().unwrap();
let mut sync_cmd = Command::cargo_bin("omk").unwrap();
sync_cmd.current_dir(&tmp);
sync_cmd.arg("kimi").arg("sync");
sync_cmd.assert().success();
fs::write(tmp.path().join(".kimi/skills"), "not a directory").unwrap();
let mut cmd = Command::cargo_bin("omk").unwrap();
cmd.current_dir(&tmp);
cmd.arg("kimi").arg("doctor");
cmd.assert()
.success()
.stdout(contains("Invalid skills path"))
.stdout(contains("is not a directory"));
}