use crate::service::lockfile::{load_lockfile, LockFile, LockPackage};
use crate::service::source::PackageSource;
use crate::service::AppService;
fn make_lock_with_pkg(name: &str) -> LockFile {
LockFile {
version: 1,
packages: vec![LockPackage {
name: name.to_string(),
source: PackageSource::LocalDir {
path: format!("packages/{name}"),
},
linked_at: "2026-04-08T12:00:00Z".to_string(),
}],
}
}
async fn make_app_service() -> AppService {
make_app_service_with_search_paths(vec![]).await
}
async fn make_app_service_with_search_paths(
search_paths: Vec<crate::service::resolve::SearchPath>,
) -> AppService {
use std::sync::Arc;
let executor = Arc::new(
algocline_engine::Executor::new(vec![])
.await
.expect("executor"),
);
AppService {
executor,
registry: Arc::new(algocline_engine::SessionRegistry::new()),
log_config: crate::service::config::AppConfig {
log_dir: None,
log_dir_source: crate::service::config::LogDirSource::None,
log_enabled: false,
},
search_paths,
eval_sessions: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
session_strategies: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
}
}
#[tokio::test]
async fn pkg_list_with_project() {
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path();
let pkg_dir = project_root.join("my_local_pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("init.lua"), "return {}").unwrap();
let lock = make_lock_with_pkg("my_local_pkg");
let lock = LockFile {
packages: vec![LockPackage {
name: "my_local_pkg".to_string(),
source: PackageSource::LocalDir {
path: "my_local_pkg".to_string(),
},
linked_at: "2026-04-08T12:00:00Z".to_string(),
}],
..lock
};
crate::service::lockfile::save_lockfile(project_root, &lock).unwrap();
let svc = make_app_service().await;
let result = svc
.pkg_list(Some(project_root.to_string_lossy().to_string()))
.await
.unwrap();
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
let packages = json["packages"].as_array().unwrap();
let project_pkg = packages
.iter()
.find(|p| p["name"] == "my_local_pkg")
.expect("my_local_pkg not found in pkg_list output");
assert_eq!(project_pkg["scope"], "project");
assert_eq!(project_pkg["source_type"], "local_dir");
assert_eq!(project_pkg["active"], true);
assert!(json["project_root"].is_string());
assert!(json["lockfile_path"].is_string());
}
#[tokio::test]
async fn pkg_list_no_project_root() {
let svc = make_app_service().await;
let result = svc.pkg_list(None).await.unwrap();
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(json["packages"].is_array());
}
#[tokio::test]
async fn pkg_remove_project_scope() {
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path();
let pkg_dir = project_root.join("my_local_pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("init.lua"), "return {}").unwrap();
let lock = LockFile {
version: 1,
packages: vec![LockPackage {
name: "my_local_pkg".to_string(),
source: PackageSource::LocalDir {
path: "my_local_pkg".to_string(),
},
linked_at: "2026-04-08T12:00:00Z".to_string(),
}],
};
crate::service::lockfile::save_lockfile(project_root, &lock).unwrap();
let svc = make_app_service().await;
let result = svc
.pkg_remove(
"my_local_pkg",
Some(project_root.to_string_lossy().to_string()),
None,
)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(json["removed"], "my_local_pkg");
assert_eq!(json["scope"], "project");
assert!(pkg_dir.exists(), "physical directory was deleted");
let lock_after = load_lockfile(project_root).unwrap().unwrap();
assert!(
lock_after.packages.is_empty(),
"alc.lock still contains the entry"
);
}
#[tokio::test]
async fn pkg_remove_project_scope_not_found_returns_error() {
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path();
let lock = make_lock_with_pkg("other_pkg");
crate::service::lockfile::save_lockfile(project_root, &lock).unwrap();
let svc = make_app_service().await;
let result = svc
.pkg_remove(
"nonexistent_pkg",
Some(project_root.to_string_lossy().to_string()),
None,
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found in alc.lock"));
}
#[tokio::test]
async fn pkg_list_global_unregistered_has_no_source_type() {
let tmp = tempfile::tempdir().unwrap();
let search_dir = tmp.path().join("pkgs");
std::fs::create_dir_all(&search_dir).unwrap();
let pkg_dir = search_dir.join("hand_copied_pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(
pkg_dir.join("init.lua"),
"return { meta = { name = 'hand_copied_pkg' } }",
)
.unwrap();
let search_path = crate::service::resolve::SearchPath {
path: search_dir,
source: crate::service::resolve::SearchPathSource::Env,
};
let svc = make_app_service_with_search_paths(vec![search_path]).await;
let result = svc.pkg_list(None).await.unwrap();
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
let packages = json["packages"].as_array().unwrap();
let pkg = packages
.iter()
.find(|p| p["name"] == "hand_copied_pkg")
.expect("hand_copied_pkg not found in pkg_list output");
let pkg_map = pkg
.as_object()
.expect("package entry must be a JSON object");
assert!(
!pkg_map.contains_key("source_type"),
"source_type should be absent for unregistered package, got: {:?}",
pkg_map.get("source_type")
);
assert_eq!(pkg["scope"], "global");
assert_eq!(pkg["active"], true);
}