use super::*;
use crate::runtime::path::render_host_visible_path;
use crate::skill::dependencies::{
DependencyArchiveType, DependencyExportSpec, DependencyPackageSpec, DependencySourceSpec,
GithubReleaseSourceSpec, PackageDependencyManifest, ToolDependencySpec, UrlSourceSpec,
};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
fn test_manager() -> (DependencyManager, PathBuf) {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let root = std::env::temp_dir().join(format!("luaskills-dependency-test-{}", unique));
let config = DependencyManagerConfig {
tool_root: root.join("dependencies").join("tools"),
host_tool_root: root.join("bin").join("tools"),
lua_root: root.join("dependencies").join("lua"),
host_lua_root: root.join("lua_packages"),
ffi_root: root.join("dependencies").join("ffi"),
host_ffi_root: root.join("libs"),
download_cache_root: root.join("temp").join("downloads"),
allow_network_download: false,
github_base_url: None,
github_api_base_url: None,
};
(DependencyManager::new(config), root)
}
fn runtime_log_callback_test_guard() -> MutexGuard<'static, ()> {
static TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
match TEST_MUTEX.get_or_init(|| Mutex::new(())).lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
struct RuntimeLogCallbackResetGuard;
impl Drop for RuntimeLogCallbackResetGuard {
fn drop(&mut self) {
crate::runtime_logging::set_log_callback(None);
}
}
#[test]
fn ensure_directory_create_error_uses_host_visible_path() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills-dependency-ensure-dir-test-{}",
std::process::id()
));
if temp_root.exists() {
let _ = fs::remove_dir_all(&temp_root);
}
fs::create_dir_all(&temp_root).expect("temp root should be created");
let root_file = temp_root.join("root-file");
fs::write(&root_file, b"not a directory").expect("root fixture file should be written");
let error = ensure_directory(&root_file).expect_err("file root should fail");
let expected_prefix = format!("Failed to create {}:", render_host_visible_path(&root_file));
assert!(
error.starts_with(&expected_prefix),
"unexpected error: {}",
error
);
let _ = fs::remove_dir_all(&temp_root);
}
fn tool_dependency(name: &str, version: &str, platform_key: &str) -> ToolDependencySpec {
let mut packages = BTreeMap::new();
packages.insert(
platform_key.to_string(),
DependencyPackageSpec {
archive_type: DependencyArchiveType::Raw,
asset_name: None,
url: Some("https://example.invalid/package".to_string()),
exports: vec![DependencyExportSpec {
archive_path: "demo.bin".to_string(),
target_path: "bin/demo.bin".to_string(),
executable: false,
}],
},
);
ToolDependencySpec {
name: name.to_string(),
version: Some(version.to_string()),
required: true,
scope: DependencyScope::Skill,
source: DependencySourceSpec {
source_type: DependencySourceType::Url,
github: None,
url: Some(UrlSourceSpec::default()),
skilllist: None,
},
packages,
}
}
fn github_tool_dependency(
name: &str,
version: Option<&str>,
platform_key: &str,
) -> ToolDependencySpec {
let mut packages = BTreeMap::new();
packages.insert(
platform_key.to_string(),
DependencyPackageSpec {
archive_type: DependencyArchiveType::Zip,
asset_name: Some("demo-{version}.zip".to_string()),
url: None,
exports: vec![DependencyExportSpec {
archive_path: "demo-{version}.bin".to_string(),
target_path: "bin/demo-{version}.bin".to_string(),
executable: false,
}],
},
);
ToolDependencySpec {
name: name.to_string(),
version: version.map(str::to_string),
required: true,
scope: DependencyScope::Skill,
source: DependencySourceSpec {
source_type: DependencySourceType::GithubRelease,
github: Some(GithubReleaseSourceSpec {
repo: "OpenVulcan/demo-dependency".to_string(),
tag_api: None,
}),
url: None,
skilllist: None,
},
packages,
}
}
fn github_tagged_tool_dependency(
name: &str,
version: Option<&str>,
platform_key: &str,
) -> ToolDependencySpec {
let mut dependency = github_tool_dependency(name, version, platform_key);
let package = dependency
.packages
.get_mut(platform_key)
.expect("test package should exist");
package.exports = vec![DependencyExportSpec {
archive_path: "demo-{tag}.bin".to_string(),
target_path: "bin/demo-{tag}.bin".to_string(),
executable: false,
}];
dependency
}
#[test]
fn ensure_dependency_reuses_existing_github_release_exports_without_remote_resolution() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (mut manager, root) = test_manager();
manager.config.allow_network_download = true;
manager.config.github_api_base_url = Some("https://example.invalid/github-api".to_string());
manager.downloader = DownloadManager::new(DownloadManagerConfig {
cache_root: manager.config.download_cache_root.clone(),
allow_network_download: manager.config.allow_network_download,
github_base_url: manager.config.github_base_url.clone(),
github_api_base_url: manager.config.github_api_base_url.clone(),
});
let skill_id = "demo-skill";
let manifest = PackageDependencyManifest {
tool_dependencies: vec![github_tool_dependency(
"demo-tool",
Some("1.2.3"),
platform_key,
)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let dependency_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"demo-tool",
Some("1.2.3"),
platform_key,
);
fs::create_dir_all(dependency_root.join("bin")).unwrap();
fs::write(dependency_root.join("bin").join("demo-1.2.3.bin"), b"ready").unwrap();
manager
.ensure_skill_dependencies(skill_id, &manifest)
.expect("existing exports should bypass GitHub release lookup");
let _ = fs::remove_dir_all(root);
}
#[test]
fn ensure_dependency_reuses_existing_unversioned_github_release_exports() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (mut manager, root) = test_manager();
manager.config.allow_network_download = true;
manager.config.github_api_base_url = Some("https://example.invalid/github-api".to_string());
manager.downloader = DownloadManager::new(DownloadManagerConfig {
cache_root: manager.config.download_cache_root.clone(),
allow_network_download: manager.config.allow_network_download,
github_base_url: manager.config.github_base_url.clone(),
github_api_base_url: manager.config.github_api_base_url.clone(),
});
let skill_id = "demo-skill";
let manifest = PackageDependencyManifest {
tool_dependencies: vec![github_tool_dependency("demo-tool", None, platform_key)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let dependency_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"demo-tool",
Some("1.2.3"),
platform_key,
);
fs::create_dir_all(dependency_root.join("bin")).unwrap();
fs::write(dependency_root.join("bin").join("demo-1.2.3.bin"), b"ready").unwrap();
manager
.ensure_skill_dependencies(skill_id, &manifest)
.expect("existing version directories should bypass GitHub release lookup");
let _ = fs::remove_dir_all(root);
}
#[test]
fn local_unversioned_dependency_probe_requests_warns_when_version_root_is_not_directory() {
let _log_guard = runtime_log_callback_test_guard();
let (manager, root) = test_manager();
let skill_id = "demo-skill";
let dependency_name = "demo-tool";
let platform_key = "test-platform";
let dependency_root = manager
.config
.tool_root
.join(skill_id)
.join(dependency_name);
fs::create_dir_all(
dependency_root
.parent()
.expect("dependency root should have parent"),
)
.expect("dependency parent should be created");
fs::write(&dependency_root, b"not a directory").expect("dependency root fixture file");
let warnings = Arc::new(Mutex::new(Vec::new()));
let captured_warnings = Arc::clone(&warnings);
crate::runtime_logging::set_log_callback(Some(Arc::new(move |event| {
if event.level == crate::runtime_logging::RuntimeLogLevel::Warn {
captured_warnings
.lock()
.expect("capture dependency warning")
.push(event.message.clone());
}
})));
let _callback_reset = RuntimeLogCallbackResetGuard;
let package = DependencyPackageSpec {
archive_type: DependencyArchiveType::Raw,
asset_name: None,
url: None,
exports: Vec::new(),
};
let requests = manager.local_unversioned_dependency_probe_requests(
skill_id,
SkillDependencyKind::Tool,
dependency_name,
DependencyScope::Skill,
&package,
platform_key,
&manager.config.tool_root,
);
assert!(requests.is_empty());
let warnings = warnings.lock().expect("read dependency warnings");
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("Failed to scan local dependency versions under"));
assert!(warnings[0].contains(&render_host_visible_path(&dependency_root)));
let _ = fs::remove_dir_all(root);
}
#[test]
fn ensure_dependency_reuses_existing_github_release_tag_exports_without_remote_resolution() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (mut manager, root) = test_manager();
manager.config.allow_network_download = true;
manager.config.github_api_base_url = Some("https://example.invalid/github-api".to_string());
manager.downloader = DownloadManager::new(DownloadManagerConfig {
cache_root: manager.config.download_cache_root.clone(),
allow_network_download: manager.config.allow_network_download,
github_base_url: manager.config.github_base_url.clone(),
github_api_base_url: manager.config.github_api_base_url.clone(),
});
let skill_id = "demo-skill";
let manifest = PackageDependencyManifest {
tool_dependencies: vec![github_tagged_tool_dependency(
"demo-tool",
Some("1.2.3"),
platform_key,
)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let dependency_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"demo-tool",
Some("1.2.3"),
platform_key,
);
fs::create_dir_all(dependency_root.join("bin")).unwrap();
fs::write(
dependency_root.join("bin").join("demo-v1.2.3.bin"),
b"ready",
)
.unwrap();
manager
.ensure_skill_dependencies(skill_id, &manifest)
.expect("existing tag exports should bypass GitHub release lookup");
let _ = fs::remove_dir_all(root);
}
#[test]
fn ensure_dependency_reuses_existing_unversioned_github_release_tag_exports() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (mut manager, root) = test_manager();
manager.config.allow_network_download = true;
manager.config.github_api_base_url = Some("https://example.invalid/github-api".to_string());
manager.downloader = DownloadManager::new(DownloadManagerConfig {
cache_root: manager.config.download_cache_root.clone(),
allow_network_download: manager.config.allow_network_download,
github_base_url: manager.config.github_base_url.clone(),
github_api_base_url: manager.config.github_api_base_url.clone(),
});
let skill_id = "demo-skill";
let manifest = PackageDependencyManifest {
tool_dependencies: vec![github_tagged_tool_dependency(
"demo-tool",
None,
platform_key,
)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let dependency_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"demo-tool",
Some("1.2.3"),
platform_key,
);
fs::create_dir_all(dependency_root.join("bin")).unwrap();
fs::write(
dependency_root.join("bin").join("demo-v1.2.3.bin"),
b"ready",
)
.unwrap();
manager
.ensure_skill_dependencies(skill_id, &manifest)
.expect("existing unversioned tag exports should bypass GitHub release lookup");
let _ = fs::remove_dir_all(root);
}
#[test]
fn detect_dependency_reports_export_target_probe_errors() {
let (manager, root) = test_manager();
let request = ResolvedDependencyRequest {
kind: SkillDependencyKind::Tool,
name: "demo-tool".to_string(),
scope: DependencyScope::Skill,
platform_key: "test-platform".to_string(),
download_url: String::new(),
version: Some("1.2.3".to_string()),
install_root: root.join("dependencies").join("tools"),
archive_type: DependencyArchiveType::Raw,
exports: vec![DependencyExportSpec {
archive_path: "demo.bin".to_string(),
target_path: "bin/invalid\0demo.bin".to_string(),
executable: false,
}],
};
let error = manager
.detect_dependency(&request)
.expect_err("invalid export target probe should fail");
assert!(
error.contains("Failed to inspect dependency export target"),
"unexpected error: {}",
error
);
assert!(error.contains("demo-tool"), "unexpected error: {}", error);
assert!(error.contains("invalid"), "unexpected error: {}", error);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_updated_skill_dependencies_removes_stale_roots_and_keeps_reused_roots() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (manager, root) = test_manager();
let skill_id = "demo-skill";
let previous_manifest = PackageDependencyManifest {
tool_dependencies: vec![
tool_dependency("rg", "14.1.1", platform_key),
tool_dependency("fd", "9.0.0", platform_key),
],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let current_manifest = PackageDependencyManifest {
tool_dependencies: vec![
tool_dependency("rg", "14.1.2", platform_key),
tool_dependency("fd", "9.0.0", platform_key),
],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let stale_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"rg",
Some("14.1.1"),
platform_key,
);
let kept_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"fd",
Some("9.0.0"),
platform_key,
);
let current_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"rg",
Some("14.1.2"),
platform_key,
);
fs::create_dir_all(stale_root.join("bin")).unwrap();
fs::write(stale_root.join("bin").join("demo.bin"), b"old").unwrap();
fs::create_dir_all(kept_root.join("bin")).unwrap();
fs::write(kept_root.join("bin").join("demo.bin"), b"keep").unwrap();
fs::create_dir_all(current_root.join("bin")).unwrap();
fs::write(current_root.join("bin").join("demo.bin"), b"new").unwrap();
manager
.cleanup_updated_skill_dependencies(
skill_id,
Some(&previous_manifest),
Some(¤t_manifest),
)
.unwrap();
assert!(
!stale_root.exists(),
"stale dependency root should be removed"
);
assert!(
kept_root.exists(),
"unchanged dependency root should be preserved"
);
assert!(
current_root.exists(),
"current dependency root should be preserved"
);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_updated_skill_dependencies_keeps_identical_roots() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (manager, root) = test_manager();
let skill_id = "demo-skill";
let manifest = PackageDependencyManifest {
tool_dependencies: vec![tool_dependency("rg", "14.1.1", platform_key)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let dependency_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"rg",
Some("14.1.1"),
platform_key,
);
fs::create_dir_all(dependency_root.join("bin")).unwrap();
fs::write(dependency_root.join("bin").join("demo.bin"), b"keep").unwrap();
manager
.cleanup_updated_skill_dependencies(skill_id, Some(&manifest), Some(&manifest))
.unwrap();
assert!(
dependency_root.exists(),
"unchanged dependency root should remain"
);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_updated_skill_dependencies_removes_deleted_dependencies() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (manager, root) = test_manager();
let skill_id = "demo-skill";
let previous_manifest = PackageDependencyManifest {
tool_dependencies: vec![
tool_dependency("rg", "14.1.1", platform_key),
tool_dependency("fd", "9.0.0", platform_key),
],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let current_manifest = PackageDependencyManifest::default();
let rg_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"rg",
Some("14.1.1"),
platform_key,
);
let fd_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"fd",
Some("9.0.0"),
platform_key,
);
fs::create_dir_all(rg_root.join("bin")).unwrap();
fs::write(rg_root.join("bin").join("demo.bin"), b"old-rg").unwrap();
fs::create_dir_all(fd_root.join("bin")).unwrap();
fs::write(fd_root.join("bin").join("demo.bin"), b"old-fd").unwrap();
manager
.cleanup_updated_skill_dependencies(
skill_id,
Some(&previous_manifest),
Some(¤t_manifest),
)
.unwrap();
assert!(
!rg_root.exists(),
"removed dependency root should be deleted"
);
assert!(
!fd_root.exists(),
"removed dependency root should be deleted"
);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_updated_skill_dependencies_preserves_existing_roots_for_add_only_changes() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (manager, root) = test_manager();
let skill_id = "demo-skill";
let previous_manifest = PackageDependencyManifest {
tool_dependencies: vec![tool_dependency("rg", "14.1.1", platform_key)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let current_manifest = PackageDependencyManifest {
tool_dependencies: vec![
tool_dependency("rg", "14.1.1", platform_key),
tool_dependency("fd", "9.0.0", platform_key),
],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let rg_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"rg",
Some("14.1.1"),
platform_key,
);
let fd_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"fd",
Some("9.0.0"),
platform_key,
);
fs::create_dir_all(rg_root.join("bin")).unwrap();
fs::write(rg_root.join("bin").join("demo.bin"), b"keep-rg").unwrap();
fs::create_dir_all(fd_root.join("bin")).unwrap();
fs::write(fd_root.join("bin").join("demo.bin"), b"new-fd").unwrap();
manager
.cleanup_updated_skill_dependencies(
skill_id,
Some(&previous_manifest),
Some(¤t_manifest),
)
.unwrap();
assert!(
rg_root.exists(),
"existing dependency root should be preserved"
);
assert!(
fd_root.exists(),
"new dependency root should remain untouched"
);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_updated_skill_dependencies_rejects_file_stale_root() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (manager, root) = test_manager();
let skill_id = "demo-skill";
let previous_manifest = PackageDependencyManifest {
tool_dependencies: vec![tool_dependency("rg", "14.1.1", platform_key)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let current_manifest = PackageDependencyManifest::default();
let stale_root = build_dependency_install_root(
&manager.config.tool_root,
DependencyScope::Skill,
skill_id,
"rg",
Some("14.1.1"),
platform_key,
);
let stale_parent = stale_root
.parent()
.expect("stale dependency root should have parent");
fs::create_dir_all(stale_parent).expect("stale root parent should be created");
fs::write(&stale_root, b"not-a-directory").expect("stale root file should be written");
let error = manager
.cleanup_updated_skill_dependencies(
skill_id,
Some(&previous_manifest),
Some(¤t_manifest),
)
.expect_err("file stale dependency root cleanup should fail");
let expected_error = format!(
"Stale dependency root is not a directory before update cleanup: {}",
render_host_visible_path(&stale_root)
);
assert_eq!(error, expected_error);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_updated_skill_dependencies_reports_invalid_stale_root_path() {
let platform_key = current_platform_key();
if platform_key == "unknown" {
return;
}
let (mut manager, root) = test_manager();
manager.config.tool_root = PathBuf::from("invalid\0tool-root");
let skill_id = "demo-skill";
let previous_manifest = PackageDependencyManifest {
tool_dependencies: vec![tool_dependency("rg", "14.1.1", platform_key)],
lua_dependencies: Vec::new(),
ffi_dependencies: Vec::new(),
..PackageDependencyManifest::default()
};
let current_manifest = PackageDependencyManifest::default();
let error = manager
.cleanup_updated_skill_dependencies(
skill_id,
Some(&previous_manifest),
Some(¤t_manifest),
)
.expect_err("invalid stale dependency root removal should fail");
assert!(
error.contains("Failed to inspect stale dependency root"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_uninstalled_skill_dependencies_rejects_file_private_root() {
let (manager, root) = test_manager();
let removed_skill_id = "demo-skill";
let private_root = manager.config.tool_root.join(removed_skill_id);
let private_parent = private_root
.parent()
.expect("private dependency root should have parent");
fs::create_dir_all(private_parent).expect("private root parent should be created");
fs::write(&private_root, b"not-a-directory").expect("private root file should be written");
let error = manager
.cleanup_uninstalled_skill_dependencies_from_roots(&[], removed_skill_id, None)
.expect_err("file private dependency root cleanup should fail");
let expected_error = format!(
"Skill-private dependency root is not a directory before uninstall cleanup: {}",
render_host_visible_path(&private_root)
);
assert_eq!(error, expected_error);
let _ = fs::remove_dir_all(root);
}
#[test]
fn cleanup_uninstalled_skill_dependencies_reports_invalid_private_root_path() {
let (manager, root) = test_manager();
let removed_skill_id = "invalid\0skill";
let error = manager
.cleanup_uninstalled_skill_dependencies_from_roots(&[], removed_skill_id, None)
.expect_err("invalid private dependency root removal should fail");
assert!(
error.contains("Failed to inspect skill-private dependency root"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
let _ = fs::remove_dir_all(root);
}
#[test]
fn resolve_export_templates_expands_version_placeholder() {
let exports = vec![DependencyExportSpec {
archive_path: "ripgrep-{version}-x86_64-pc-windows-msvc/rg.exe".to_string(),
target_path: "bin/rg-{version}.exe".to_string(),
executable: false,
}];
let resolved = resolve_export_templates(&exports, Some("14.1.1"), Some("14.1.1"));
assert_eq!(
resolved[0].archive_path,
"ripgrep-14.1.1-x86_64-pc-windows-msvc/rg.exe"
);
assert_eq!(resolved[0].target_path, "bin/rg-14.1.1.exe");
}
#[test]
fn cleanup_failed_dependency_install_attempt_rejects_download_directory() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills-dependency-cleanup-download-dir-{}",
std::process::id()
));
if temp_root.exists() {
let _ = fs::remove_dir_all(&temp_root);
}
fs::create_dir_all(&temp_root).expect("temp root should be created");
let download_path = temp_root.join("cached-payload");
fs::create_dir_all(&download_path).expect("download directory should be created");
let install_root = temp_root.join("install-root");
fs::create_dir_all(&install_root).expect("install root should be created");
let error = cleanup_failed_dependency_install_attempt(&download_path, &install_root)
.expect_err("download directory cleanup should fail");
let expected_prefix = format!(
"Failed to remove failed dependency download {} before redownload:",
render_host_visible_path(&download_path)
);
assert!(
error.starts_with(&expected_prefix),
"unexpected error: {}",
error
);
assert!(
install_root.exists(),
"install root should remain untouched after download cleanup failure"
);
let _ = fs::remove_dir_all(&temp_root);
}
#[test]
fn cleanup_failed_dependency_install_attempt_rejects_install_root_file() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills-dependency-cleanup-install-file-{}",
std::process::id()
));
if temp_root.exists() {
let _ = fs::remove_dir_all(&temp_root);
}
fs::create_dir_all(&temp_root).expect("temp root should be created");
let download_path = temp_root.join("cached-payload.bin");
fs::write(&download_path, b"bad-payload").expect("download payload should be written");
let install_root = temp_root.join("install-root");
fs::write(&install_root, b"not-a-directory").expect("install root file should be written");
let error = cleanup_failed_dependency_install_attempt(&download_path, &install_root)
.expect_err("install root file cleanup should fail");
let expected_error = format!(
"Failed dependency install root is not a directory before reinstall: {}",
render_host_visible_path(&install_root)
);
assert_eq!(error, expected_error);
assert!(
!download_path.exists(),
"download payload should be removed before install root cleanup fails"
);
let _ = fs::remove_dir_all(&temp_root);
}
#[test]
fn local_dependency_version_component_accepts_utf8_file_name() {
let component =
local_dependency_version_component_from_file_name(std::ffi::OsString::from("1.2.3"));
assert_eq!(component.as_deref(), Some("1.2.3"));
}
#[cfg(unix)]
#[test]
fn local_dependency_version_component_rejects_invalid_unicode_file_name() {
use std::os::unix::ffi::OsStringExt;
let component =
local_dependency_version_component_from_file_name(std::ffi::OsString::from_vec(vec![0xff]));
assert!(component.is_none());
}