use super::{
PreparedSkillApply, PreparedSkillInstall, PreparedSkillUninstall, PreparedSkillUpdate,
SkillApplyResult, SkillInstallRequest, SkillInstallSourceType, SkillManager,
SkillManagerConfig, SkillOperationPlane, SkillUninstallResult, TempDirGuard,
collect_effective_skill_instances, collect_effective_skill_instances_from_roots,
format_uninstall_finalization_error, github_repo_skill_id, is_allowed_private_source_url,
is_skill_manifest_enabled, normalize_github_repo_locator, remove_staging_dir_if_present,
resolve_effective_skill_instance, resolve_requested_skill_id, staging_temp_root_is_directory,
unix_millis_from_system_time,
};
use crate::runtime::path::render_host_visible_path;
use crate::runtime_options::RuntimeSkillRoot;
use crate::skill::source::{InstalledSkillRecord, InstalledSkillSourceRecord};
fn test_manager_config(
temp_root: &std::path::Path,
skill_root: RuntimeSkillRoot,
) -> SkillManagerConfig {
SkillManagerConfig {
skill_root,
lifecycle_root: temp_root.join("state"),
download_cache_root: temp_root.join("downloads"),
allow_network_download: false,
github_base_url: None,
github_api_base_url: None,
official_skill_hub_base_url: None,
enable_private_url_skill_install: false,
private_skill_source_allowlist: Vec::new(),
}
}
fn test_install_source_record() -> InstalledSkillSourceRecord {
InstalledSkillSourceRecord {
source_type: SkillInstallSourceType::Github,
locator: "vulcan-ai/vulcan-codekit".to_string(),
tag: Some("v0.1.0".to_string()),
}
}
fn test_installed_record(version: &str) -> InstalledSkillRecord {
InstalledSkillRecord {
skill_id: "vulcan-codekit".to_string(),
version: version.to_string(),
managed: true,
source: test_install_source_record(),
installed_at_unix_ms: 1,
}
}
fn test_apply_result(status: &str) -> SkillApplyResult {
SkillApplyResult {
skill_id: "vulcan-codekit".to_string(),
status: status.to_string(),
message: format!("skill was {}", status),
version: Some("0.1.0".to_string()),
source_type: Some(SkillInstallSourceType::Github),
source_locator: Some("vulcan-ai/vulcan-codekit".to_string()),
}
}
fn test_uninstall_result(skill_removed: bool) -> SkillUninstallResult {
SkillUninstallResult {
skill_id: "vulcan-codekit".to_string(),
skill_removed,
sqlite_removed: false,
lancedb_removed: false,
sqlite_retained: false,
lancedb_retained: false,
message: if skill_removed {
"skill package removed".to_string()
} else {
"skill package directory not found".to_string()
},
}
}
#[test]
fn uninstall_finalization_error_keeps_base_message_when_rollback_succeeds() {
let message = format_uninstall_finalization_error(
"Failed to finalize uninstall: commit failed".to_string(),
Ok::<(), String>(()),
);
assert_eq!(message, "Failed to finalize uninstall: commit failed");
}
#[test]
fn uninstall_finalization_error_appends_rollback_failure() {
let message = format_uninstall_finalization_error(
"Failed to finalize uninstall: commit failed".to_string(),
Err("backup restore failed".to_string()),
);
assert_eq!(
message,
"Failed to finalize uninstall: commit failed. rollback failed: backup restore failed"
);
}
#[test]
fn temp_dir_guard_removes_staging_root_on_drop() {
let temp_root =
std::env::temp_dir().join(format!("luaskills_temp_guard_test_{}", std::process::id()));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
std::fs::create_dir_all(&temp_root).expect("temp root should be created");
{
let _guard = TempDirGuard::new(temp_root.clone());
std::fs::write(temp_root.join("staged.txt"), "staged")
.expect("staged marker should be written");
}
assert!(!temp_root.exists());
}
#[test]
fn remove_staging_dir_reports_missing_directory() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_missing_staging_cleanup_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let removed = remove_staging_dir_if_present(&temp_root)
.expect("missing staging directory cleanup should succeed");
assert!(!removed);
}
#[test]
fn remove_staging_dir_reports_invalid_path_errors() {
let invalid_path = std::path::PathBuf::from("invalid\0staging");
let error = remove_staging_dir_if_present(&invalid_path)
.expect_err("invalid staging directory cleanup should fail");
assert!(
error.contains("Failed to remove staging directory"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
}
#[test]
fn staging_temp_root_rejects_file_path() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_file_staging_temp_root_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let staging_temp_root = temp_root
.join("state")
.join("install_tmp")
.join("vulcan-codekit-1");
std::fs::create_dir_all(
staging_temp_root
.parent()
.expect("staging temp path should have a parent"),
)
.expect("staging temp parent should be created");
std::fs::write(&staging_temp_root, "not a staging directory")
.expect("file staging temp root should be written");
let error = staging_temp_root_is_directory(&staging_temp_root)
.expect_err("file staging temp root should fail");
assert!(
error.contains("Skill staging temp root is not a directory"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&staging_temp_root)),
"unexpected error: {}",
error
);
assert!(
staging_temp_root.is_file(),
"file staging temp root should remain in place after rejection"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn collect_effective_skill_instances_rejects_skill_root_probe_errors() {
let invalid_root = RuntimeSkillRoot {
name: "ROOT".to_string(),
skills_dir: std::path::PathBuf::from("invalid\0skills"),
};
let error = collect_effective_skill_instances_from_roots(&[invalid_root])
.expect_err("invalid skill root metadata probe should fail");
assert!(
error.contains("Failed to inspect skill root"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
}
#[test]
fn collect_effective_skill_instances_rejects_file_skill_root() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_file_skill_root_test_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&temp_root);
std::fs::create_dir_all(&temp_root).expect("create file skill-root test dir");
let file_root = temp_root.join("skills");
std::fs::write(&file_root, "not a directory\n").expect("write file skill root");
let root = RuntimeSkillRoot {
name: "ROOT".to_string(),
skills_dir: file_root.clone(),
};
let error = collect_effective_skill_instances_from_roots(&[root])
.expect_err("file skill root should fail");
assert!(
error.contains("Skill root is not a directory"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&file_root)),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn is_skill_manifest_enabled_defaults_missing_manifest_to_enabled() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_missing_manifest_enabled_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_dir = temp_root.join("skills").join("vulcan-codekit");
std::fs::create_dir_all(&skill_dir).expect("create missing manifest skill dir");
assert!(
is_skill_manifest_enabled(&skill_dir).expect("missing manifest probe should succeed"),
"missing manifest should default to enabled"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn is_skill_manifest_enabled_rejects_manifest_probe_errors() {
let invalid_skill_dir = std::path::PathBuf::from("invalid\0skill");
let error = is_skill_manifest_enabled(&invalid_skill_dir)
.expect_err("invalid skill manifest probe should fail");
assert!(
error.contains("Failed to inspect skill manifest"),
"unexpected error: {}",
error
);
assert!(error.contains("skill.yaml"), "unexpected error: {}", error);
}
#[test]
fn is_skill_manifest_enabled_rejects_directory_manifest() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_directory_manifest_enabled_test_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&temp_root);
let skill_dir = temp_root.join("skills").join("vulcan-codekit");
let manifest_dir = skill_dir.join("skill.yaml");
std::fs::create_dir_all(&manifest_dir).expect("create directory skill manifest");
let error =
is_skill_manifest_enabled(&skill_dir).expect_err("directory skill manifest should fail");
assert!(
error.contains("Skill manifest is not a file"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&manifest_dir)),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn skill_manager_persists_disabled_state() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_skill_manager_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_root = temp_root.join("skills");
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root,
},
)
});
assert!(manager.is_skill_enabled("vulcan-codekit").unwrap());
manager
.disable_skill("vulcan-codekit", Some("manual test"))
.expect("disable should succeed");
assert!(!manager.is_skill_enabled("vulcan-codekit").unwrap());
assert_eq!(
manager
.disabled_record("vulcan-codekit")
.unwrap()
.expect("record should exist")
.reason
.as_deref(),
Some("manual test")
);
manager
.enable_skill("vulcan-codekit")
.expect("enable should succeed");
assert!(manager.is_skill_enabled("vulcan-codekit").unwrap());
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn disabled_record_returns_none_when_missing() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_missing_disabled_record_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
assert!(
manager
.disabled_record("vulcan-codekit")
.expect("missing disabled record probe should succeed")
.is_none(),
"missing disabled record should return none"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn disabled_record_rejects_probe_errors() {
let invalid_lifecycle_root = std::path::PathBuf::from("invalid\0state");
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&invalid_lifecycle_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: std::path::PathBuf::from("skills"),
},
)
});
let error = manager
.disabled_record("vulcan-codekit")
.expect_err("invalid disabled record probe should fail");
assert!(
error.contains("Failed to inspect disabled record"),
"unexpected error: {}",
error
);
assert!(
error.contains("vulcan-codekit.json"),
"unexpected error: {}",
error
);
}
#[test]
fn disabled_record_rejects_directory_record() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_directory_disabled_record_test_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&temp_root);
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
let record_dir = manager.disabled_record_path("vulcan-codekit");
std::fs::create_dir_all(&record_dir).expect("create directory disabled record");
let error = manager
.disabled_record("vulcan-codekit")
.expect_err("directory disabled record should fail");
assert!(
error.contains("Disabled record is not a file"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&record_dir)),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn install_record_returns_none_when_missing() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_missing_install_record_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
assert!(
manager
.install_record("vulcan-codekit")
.expect("missing install record probe should succeed")
.is_none(),
"missing install record should return none"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn install_record_rejects_probe_errors() {
let invalid_lifecycle_root = std::path::PathBuf::from("invalid\0state");
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&invalid_lifecycle_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: std::path::PathBuf::from("skills"),
},
)
});
let error = manager
.install_record("vulcan-codekit")
.expect_err("invalid install record probe should fail");
assert!(
error.contains("Failed to inspect install record"),
"unexpected error: {}",
error
);
assert!(
error.contains("vulcan-codekit.yaml"),
"unexpected error: {}",
error
);
}
#[test]
fn install_record_rejects_directory_record() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_directory_install_record_test_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&temp_root);
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
let record_dir = manager.install_record_path("vulcan-codekit");
std::fs::create_dir_all(&record_dir).expect("create directory install record");
let error = manager
.install_record("vulcan-codekit")
.expect_err("directory install record should fail");
assert!(
error.contains("Install record is not a file"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&record_dir)),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn uninstall_skill_reports_not_removed_when_package_dir_is_missing() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_missing_uninstall_package_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
let result = manager
.uninstall_skill("vulcan-codekit")
.expect("missing package uninstall should succeed");
assert!(!result.skill_removed);
assert_eq!(result.message, "skill package directory not found");
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn uninstall_skill_rejects_file_package_path() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_file_uninstall_package_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_root = temp_root.join("skills");
std::fs::create_dir_all(&skill_root).expect("skill root should be created");
let package_path = skill_root.join("vulcan-codekit");
std::fs::write(&package_path, "not a skill directory")
.expect("file package path should be written");
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root,
},
));
let error = manager
.uninstall_skill("vulcan-codekit")
.expect_err("file package path should fail uninstall");
assert!(
error.contains("Skill package path is not a directory"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&package_path)),
"unexpected error: {}",
error
);
assert!(
package_path.is_file(),
"file package path should remain in place after rejection"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn uninstall_skill_rejects_package_dir_probe_errors() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_uninstall_package_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let invalid_skill_root = std::path::PathBuf::from("invalid\0skills");
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: invalid_skill_root,
},
)
});
let error = manager
.uninstall_skill("vulcan-codekit")
.expect_err("invalid uninstall package directory probe should fail");
assert!(
error.contains("Failed to inspect skill package directory"),
"unexpected error: {}",
error
);
assert!(
error.contains("vulcan-codekit"),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn disabled_record_parse_error_uses_host_visible_path() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_disabled_record_parse_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_root = temp_root.join("skills");
let manager = SkillManager::new(SkillManagerConfig {
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root,
},
)
});
manager
.ensure_state_layout()
.expect("state layout should be created");
let disabled_record_path = temp_root
.join("state")
.join("skills")
.join("disabled")
.join("vulcan-codekit.json");
std::fs::write(&disabled_record_path, "{not-json")
.expect("invalid disabled record should be written");
let error = manager
.disabled_record("vulcan-codekit")
.expect_err("invalid disabled record should fail");
let expected_prefix = format!(
"Failed to parse {}:",
render_host_visible_path(&disabled_record_path)
);
assert!(
error.starts_with(&expected_prefix),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn public_url_install_is_rejected_by_source_policy() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_public_url_policy_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_root = temp_root.join("skills");
std::fs::create_dir_all(&skill_root).expect("skill root should be created");
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root,
},
));
let error = manager
.prepare_install_skill(
SkillOperationPlane::Skills,
&[RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
}],
&SkillInstallRequest {
skill_id: Some("demo-skill".to_string()),
source: Some("https://example.com/demo.yaml".to_string()),
source_type: SkillInstallSourceType::Url,
},
)
.expect_err("public URL install should be rejected");
assert!(error.contains("public URL skill install is disabled"));
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn official_hub_request_derives_skill_id_from_source() {
let skill_id = resolve_requested_skill_id(&SkillInstallRequest {
skill_id: None,
source: Some("skill-search".to_string()),
source_type: SkillInstallSourceType::OfficialHub,
})
.expect("official Hub skill id should derive from source");
assert_eq!(skill_id, "skill-search");
}
#[test]
fn github_request_derives_skill_id_from_repository_source() {
let skill_id = resolve_requested_skill_id(&SkillInstallRequest {
skill_id: None,
source: Some("https://github.com/vulcan-ai/vulcan-codekit".to_string()),
source_type: SkillInstallSourceType::Github,
})
.expect("GitHub skill id should derive from repository source");
assert_eq!(skill_id, "vulcan-codekit");
}
#[test]
fn github_repo_locator_normalization_requires_exact_owner_repo_segments() {
assert_eq!(
normalize_github_repo_locator(" https://github.com/vulcan-ai/vulcan-codekit/ ")
.expect("GitHub URL locator should normalize"),
"vulcan-ai/vulcan-codekit"
);
assert_eq!(
normalize_github_repo_locator("vulcan-ai/vulcan-codekit")
.expect("owner/repo locator should normalize"),
"vulcan-ai/vulcan-codekit"
);
let missing_owner =
normalize_github_repo_locator("/vulcan-codekit").expect_err("missing owner should fail");
assert!(missing_owner.contains("owner/repo form"));
let missing_repo =
normalize_github_repo_locator("vulcan-ai/").expect_err("missing repo should fail");
assert!(missing_repo.contains("owner/repo form"));
let extra_path = normalize_github_repo_locator("vulcan-ai/vulcan-codekit/tree/main")
.expect_err("extra GitHub path segments should fail");
assert!(extra_path.contains("owner/repo form"));
}
#[test]
fn github_repo_skill_id_rejects_locator_without_repo_segment() {
let error =
github_repo_skill_id("vulcan-codekit").expect_err("repo-only GitHub locator should fail");
assert!(error.contains("owner/repo form"));
}
#[test]
fn official_hub_install_rejects_mismatched_source_skill_id() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_official_hub_mismatch_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_root = temp_root.join("skills");
std::fs::create_dir_all(&skill_root).expect("skill root should be created");
let manager = SkillManager::new(SkillManagerConfig {
official_skill_hub_base_url: Some("https://hub.example.invalid".to_string()),
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root,
},
)
});
let error = manager
.prepare_install_skill(
SkillOperationPlane::Skills,
&[RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
}],
&SkillInstallRequest {
skill_id: Some("expected-skill".to_string()),
source: Some("other-skill".to_string()),
source_type: SkillInstallSourceType::OfficialHub,
},
)
.expect_err("mismatched official Hub locator should fail before network");
assert!(error.contains("does not match requested skill_id"));
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn private_source_allowlist_uses_directory_prefix_matching() {
let allowlist = vec!["https://internal.example.com/luaskills/manifests".to_string()];
assert!(is_allowed_private_source_url(
"https://internal.example.com/luaskills/manifests/demo.json",
&allowlist,
));
assert!(!is_allowed_private_source_url(
"https://internal.example.com/luaskills/manifests-extra/demo.json",
&allowlist,
));
}
#[test]
fn install_update_entrypoints_return_strict_structured_results() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_install_update_test_{}",
std::process::id()
));
let skill_root = temp_root.join("skills");
let skill_roots = vec![RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root.clone(),
}];
let _ = std::fs::create_dir_all(&skill_root);
let manager = SkillManager::new(test_manager_config(&temp_root, skill_roots[0].clone()));
let install_result = manager
.prepare_install_skill(
SkillOperationPlane::Skills,
&skill_roots,
&SkillInstallRequest {
skill_id: Some("vulcan-codekit".to_string()),
source: None,
source_type: SkillInstallSourceType::Github,
},
)
.expect_err("install without source should fail strictly");
assert!(install_result.contains("github install requires source repository"));
let _ = std::fs::create_dir_all(skill_root.join("vulcan-codekit"));
let update_result = manager
.prepare_update_skill(
SkillOperationPlane::Skills,
&skill_roots,
&SkillInstallRequest {
skill_id: Some("vulcan-codekit".to_string()),
source: None,
source_type: SkillInstallSourceType::Github,
},
)
.expect_err("update without install record should fail strictly");
assert!(update_result.contains("is not managed by the install workflow"));
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn stage_skill_install_rejects_temp_root_probe_errors() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_install_stage_temp_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let invalid_lifecycle_root = std::path::PathBuf::from("invalid\0state");
let manager = SkillManager::new(SkillManagerConfig {
lifecycle_root: invalid_lifecycle_root,
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
let error = manager
.stage_skill_install_from_archive(
"vulcan-codekit",
&temp_root.join("unused.zip"),
"0.1.0",
test_install_source_record(),
"install message".to_string(),
)
.expect_err("invalid install temp root probe should fail");
assert!(
error.contains("Failed to inspect skill staging temp root"),
"unexpected error: {}",
error
);
assert!(error.contains("install_tmp"), "unexpected error: {}", error);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn stage_skill_update_rejects_temp_root_probe_errors() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_update_stage_temp_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let invalid_lifecycle_root = std::path::PathBuf::from("invalid\0state");
let manager = SkillManager::new(SkillManagerConfig {
lifecycle_root: invalid_lifecycle_root,
..test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
)
});
let error = manager
.stage_skill_update_from_archive(
"vulcan-codekit",
&temp_root.join("unused.zip"),
"0.2.0",
test_installed_record("0.1.0"),
test_install_source_record(),
)
.expect_err("invalid update temp root probe should fail");
assert!(
error.contains("Failed to inspect skill staging temp root"),
"unexpected error: {}",
error
);
assert!(error.contains("update_tmp"), "unexpected error: {}", error);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn rollback_staged_install_rejects_target_dir_probe_errors() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_install_rollback_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
));
let prepared = PreparedSkillApply::Install(PreparedSkillInstall {
result: test_apply_result("installed"),
target_dir: std::path::PathBuf::from("invalid\0target"),
install_record: test_installed_record("0.1.0"),
});
let error = manager
.rollback_prepared_skill_apply(&prepared)
.expect_err("invalid staged install target probe should fail");
assert!(
error.contains("Failed to inspect skill package directory"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn rollback_staged_update_rejects_backup_probe_errors_before_removing_target() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_update_rollback_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let target_dir = temp_root.join("skills").join("vulcan-codekit");
std::fs::create_dir_all(&target_dir).expect("staged target should be created");
std::fs::write(target_dir.join("marker.txt"), "staged")
.expect("staged marker should be written");
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
));
let prepared = PreparedSkillApply::Update(PreparedSkillUpdate {
result: test_apply_result("updated"),
target_dir: target_dir.clone(),
backup_dir: std::path::PathBuf::from("invalid\0backup"),
install_record: test_installed_record("0.2.0"),
previous_install_record: test_installed_record("0.1.0"),
});
let error = manager
.rollback_prepared_skill_apply(&prepared)
.expect_err("invalid update backup probe should fail");
assert!(
error.contains("Failed to inspect skill package directory"),
"unexpected error: {}",
error
);
assert!(
target_dir.join("marker.txt").exists(),
"staged target should remain when backup probe fails"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn commit_staged_update_restores_previous_record_on_backup_probe_error() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_update_commit_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
));
let previous_record = test_installed_record("0.1.0");
let prepared = PreparedSkillApply::Update(PreparedSkillUpdate {
result: test_apply_result("updated"),
target_dir: temp_root.join("skills").join("vulcan-codekit"),
backup_dir: std::path::PathBuf::from("invalid\0backup"),
install_record: test_installed_record("0.2.0"),
previous_install_record: previous_record.clone(),
});
let error = manager
.commit_prepared_skill_apply(&prepared)
.expect_err("invalid update backup probe should fail during commit");
assert!(
error.contains("Failed to inspect skill package directory"),
"unexpected error: {}",
error
);
assert!(
error.contains("previous install record was restored"),
"unexpected error: {}",
error
);
assert_eq!(
manager
.install_record("vulcan-codekit")
.expect("restored install record should be readable")
.expect("restored install record should exist"),
previous_record
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn rollback_staged_uninstall_rejects_target_dir_probe_errors() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_uninstall_rollback_target_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let backup_dir = temp_root
.join("state")
.join("uninstall_backup")
.join("backup");
std::fs::create_dir_all(&backup_dir).expect("backup directory should be created");
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
));
let prepared = PreparedSkillUninstall {
result: test_uninstall_result(true),
target_dir: std::path::PathBuf::from("invalid\0target"),
backup_dir: Some(backup_dir),
previous_disabled_record: None,
previous_install_record: None,
};
let error = manager
.rollback_prepared_skill_uninstall(&prepared)
.expect_err("invalid staged uninstall target probe should fail");
assert!(
error.contains("Failed to inspect skill package directory"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn rollback_staged_uninstall_rejects_backup_probe_errors_before_removing_target() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_uninstall_rollback_backup_probe_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let target_dir = temp_root.join("skills").join("vulcan-codekit");
std::fs::create_dir_all(&target_dir).expect("staged target should be created");
std::fs::write(target_dir.join("marker.txt"), "staged")
.expect("staged marker should be written");
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("skills"),
},
));
let prepared = PreparedSkillUninstall {
result: test_uninstall_result(true),
target_dir: target_dir.clone(),
backup_dir: Some(std::path::PathBuf::from("invalid\0backup")),
previous_disabled_record: None,
previous_install_record: None,
};
let error = manager
.rollback_prepared_skill_uninstall(&prepared)
.expect_err("invalid uninstall backup probe should fail");
assert!(
error.contains("Failed to inspect skill package directory"),
"unexpected error: {}",
error
);
assert!(
target_dir.join("marker.txt").exists(),
"staged target should remain when backup probe fails"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn uninstall_returns_safe_default_database_flags() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_uninstall_result_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_root = temp_root.join("skills");
let manager = SkillManager::new(test_manager_config(
&temp_root,
RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: skill_root.clone(),
},
));
let _ = std::fs::create_dir_all(skill_root.join("vulcan-codekit"));
let result = manager
.uninstall_skill("vulcan-codekit")
.expect("uninstall should succeed");
assert!(result.skill_removed);
assert!(!result.sqlite_removed);
assert!(!result.lancedb_removed);
assert!(!skill_root.join("vulcan-codekit").exists());
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn collect_effective_skill_instances_keeps_root_priority_over_project() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_collect_effective_instances_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let base_dir = temp_root.join("base");
let override_dir = temp_root.join("override");
let _ = std::fs::create_dir_all(base_dir.join("vulcan-codekit"));
let _ = std::fs::create_dir_all(override_dir.join("vulcan-codekit"));
let _ = std::fs::create_dir_all(override_dir.join("vulcan-runtime"));
let _ = std::fs::write(
base_dir.join("vulcan-codekit").join("skill.yaml"),
"name: vulcan-codekit\nversion: 0.1.0\n",
);
let _ = std::fs::write(
override_dir.join("vulcan-codekit").join("skill.yaml"),
"name: vulcan-codekit\nversion: 0.2.0\n",
);
let _ = std::fs::write(
override_dir.join("vulcan-runtime").join("skill.yaml"),
"name: vulcan-runtime\nversion: 0.1.0\n",
);
let resolved = collect_effective_skill_instances(&base_dir, Some(&override_dir))
.expect("effective skill collection should succeed");
assert_eq!(resolved.len(), 2);
let codekit = resolved
.iter()
.find(|value| value.skill_id == "vulcan-codekit")
.expect("vulcan-codekit should exist");
assert!(codekit.actual_dir.starts_with(&base_dir));
let runtime = resolved
.iter()
.find(|value| value.skill_id == "vulcan-runtime")
.expect("project-only vulcan-runtime should exist");
assert!(runtime.actual_dir.starts_with(&override_dir));
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn resolve_effective_skill_instance_prefers_root_directory() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_resolve_effective_instance_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let base_dir = temp_root.join("base");
let override_dir = temp_root.join("override");
let _ = std::fs::create_dir_all(base_dir.join("vulcan-codekit"));
let _ = std::fs::create_dir_all(override_dir.join("vulcan-codekit"));
let _ = std::fs::write(
base_dir.join("vulcan-codekit").join("skill.yaml"),
"name: vulcan-codekit\nversion: 0.1.0\n",
);
let _ = std::fs::write(
override_dir.join("vulcan-codekit").join("skill.yaml"),
"name: vulcan-codekit\nversion: 0.2.0\n",
);
let resolved =
resolve_effective_skill_instance(&base_dir, Some(&override_dir), "vulcan-codekit")
.expect("resolution should succeed")
.expect("instance should exist");
assert!(resolved.actual_dir.starts_with(&base_dir));
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn unix_millis_from_system_time_accepts_post_epoch_time() {
let timestamp = std::time::UNIX_EPOCH + std::time::Duration::from_millis(1);
assert_eq!(
unix_millis_from_system_time(timestamp, "test timestamp")
.expect("post-epoch timestamp should convert"),
1
);
}
#[test]
fn unix_millis_from_system_time_rejects_pre_epoch_time() {
let timestamp = std::time::UNIX_EPOCH - std::time::Duration::from_millis(1);
let error = unix_millis_from_system_time(timestamp, "test timestamp")
.expect_err("pre-epoch timestamp should fail");
assert!(
error.starts_with("System clock is before Unix epoch while computing test timestamp:"),
"unexpected error: {}",
error
);
}