use super::*;
#[test]
fn provider_selection_parses_multi_select() {
assert_eq!(parse_provider_selection("1,2", 3), vec![1, 2]);
assert_eq!(parse_provider_selection(" 2 , 3 ", 3), vec![2, 3]);
assert_eq!(parse_provider_selection("1", 3), vec![1]);
}
#[test]
fn provider_selection_drops_out_of_range_and_garbage() {
assert_eq!(parse_provider_selection("0,4,2,abc", 3), vec![2]);
assert!(parse_provider_selection("", 3).is_empty());
assert!(parse_provider_selection("9,10", 3).is_empty());
}
#[test]
fn provider_selection_dedupes_preserving_order() {
assert_eq!(parse_provider_selection("2,1,2,1", 3), vec![2, 1]);
}
#[test]
fn provider_selection_preserves_entry_order() {
assert_eq!(parse_provider_selection("3,1", 3), vec![3, 1]);
}
#[test]
fn extract_var_refs_finds_all() {
assert_eq!(extract_var_refs("{{ foo }}"), vec!["foo"]);
assert_eq!(extract_var_refs("{{ a }}-{{ b }}"), vec!["a", "b"],);
assert!(extract_var_refs("no vars").is_empty());
}
#[test]
fn resolve_template_replaces_vars() {
let mut vars = HashMap::new();
vars.insert("key".to_string(), "secret123".to_string());
vars.insert("url".to_string(), "https://api.example.com".to_string());
assert_eq!(resolve_template("{{ key }}", &vars), "secret123",);
assert_eq!(
resolve_template("prefix-{{ url }}-suffix", &vars),
"prefix-https://api.example.com-suffix",
);
}
#[test]
fn resolve_template_handles_missing_var() {
let vars = HashMap::new();
assert_eq!(resolve_template("{{ missing }}", &vars), "{{ missing }}",);
}
#[test]
fn distro_source_resolution_bare_name() {
assert_eq!(
resolve_distro_url("astralis"),
"https://raw.githubusercontent.com/unicity-astrid/astralis/main/Distro.toml",
);
}
#[test]
fn distro_source_resolution_at_prefix() {
assert_eq!(
resolve_distro_url("@myorg/mydistro"),
"https://raw.githubusercontent.com/myorg/mydistro/main/Distro.toml",
);
}
#[test]
fn distro_source_resolution_full_url() {
let url = "https://example.com/Distro.toml";
assert_eq!(resolve_distro_url(url), url);
}
use super::super::distro::manifest::{DistroCapsule, VariableDef};
fn cap(name: &str, group: Option<&str>, default: bool) -> DistroCapsule {
DistroCapsule {
name: name.to_string(),
source: format!("@org/{name}"),
version: "0.1.0".to_string(),
tag: None,
branch: None,
rev: None,
default,
group: group.map(String::from),
role: None,
env: HashMap::new(),
}
}
#[test]
fn parse_cli_vars_splits_first_equals() {
let raw = vec!["A=1".to_string(), "URL=https://x?y=z".to_string()];
let map = parse_cli_vars(&raw).unwrap();
assert_eq!(map["A"], "1");
assert_eq!(map["URL"], "https://x?y=z");
}
#[test]
fn parse_cli_vars_rejects_no_equals() {
assert!(parse_cli_vars(&["NOEQ".to_string()]).is_err());
assert!(parse_cli_vars(&["=value".to_string()]).is_err());
}
#[test]
fn headless_select_takes_defaults_and_ungrouped() {
let caps = vec![
cap("cli", None, false),
cap("openai", Some("llm"), true),
cap("anthropic", Some("llm"), false),
];
let selected = select_capsules(caps, true).unwrap();
let names: std::collections::HashSet<&str> = selected.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains("cli"), "ungrouped always selected");
assert!(names.contains("openai"), "group default selected");
assert!(!names.contains("anthropic"), "non-default not selected");
}
#[test]
fn headless_select_falls_back_to_first_when_no_default() {
let caps = vec![
cap("cli", None, false),
cap("alpha", Some("llm"), false),
cap("beta", Some("llm"), false),
];
let selected = select_capsules(caps, true).unwrap();
let names: std::collections::HashSet<&str> = selected.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains("alpha"));
assert!(!names.contains("beta"));
}
fn var(secret: bool, default: Option<&str>) -> VariableDef {
VariableDef {
secret,
description: None,
default: default.map(String::from),
}
}
fn cap_with_env(name: &str, key: &str, template: &str) -> DistroCapsule {
let mut c = cap(name, None, false);
c.env.insert(key.to_string(), template.to_string());
c
}
#[test]
fn headless_collect_uses_cli_var_override() {
let mut variables = HashMap::new();
variables.insert("api_key".to_string(), var(true, Some("from-default")));
let selected = vec![cap_with_env("llm", "API_KEY", "{{ api_key }}")];
let mut cli = HashMap::new();
cli.insert("api_key".to_string(), "from-cli".to_string());
let vars = collect_variables(&variables, &selected, true, &cli).unwrap();
assert_eq!(vars["api_key"], "from-cli");
}
#[test]
fn headless_collect_uses_env_then_default() {
let mut variables = HashMap::new();
variables.insert("base_url".to_string(), var(false, Some("https://default")));
let mut needed = std::collections::HashSet::new();
needed.insert("base_url".to_string());
let vars = collect_variables_headless(&variables, &needed, &HashMap::new(), |_| None).unwrap();
assert_eq!(vars["base_url"], "https://default");
let vars = collect_variables_headless(&variables, &needed, &HashMap::new(), |k| {
(k == "ASTRID_VAR_BASE_URL").then(|| "https://from-env".to_string())
})
.unwrap();
assert_eq!(vars["base_url"], "https://from-env");
}
#[tokio::test]
async fn offline_refuses_remote_capsule_source() {
let selected = vec![cap("llm", None, false)]; let err = install_capsules(&selected, true, &astrid_core::PrincipalId::default())
.await
.unwrap_err();
assert!(err.to_string().contains("--offline"), "got: {err}");
assert!(err.to_string().contains("network/GitHub"), "got: {err}");
}
#[test]
fn offline_guard_blocks_only_github_sources() {
assert!(is_network_capsule_source("@org/repo"));
assert!(is_network_capsule_source("@org/repo@1.2.0"));
assert!(is_network_capsule_source("github.com/org/repo"));
assert!(is_network_capsule_source("https://github.com/org/repo"));
assert!(!is_network_capsule_source("capsules/cli.capsule"));
assert!(!is_network_capsule_source("./capsules/cli.capsule"));
assert!(!is_network_capsule_source("/abs/path/cli.capsule"));
assert!(!is_network_capsule_source("cli.capsule"));
}
#[test]
fn headless_collect_errors_on_missing_required_var() {
let mut variables = HashMap::new();
variables.insert("api_key".to_string(), var(true, None)); let mut needed = std::collections::HashSet::new();
needed.insert("api_key".to_string());
let err =
collect_variables_headless(&variables, &needed, &HashMap::new(), |_| None).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("api_key"), "got: {msg}");
assert!(msg.contains("ASTRID_VAR_API_KEY"), "got: {msg}");
}
#[test]
fn write_env_files_targets_the_given_principal() {
let dir = tempfile::tempdir().unwrap();
let home = AstridHome::from_path(dir.path());
let scope = astrid_core::PrincipalId::new("claude-code").unwrap();
let default = astrid_core::PrincipalId::default();
assert_ne!(scope.as_str(), default.as_str(), "scope must differ");
let selected = vec![cap_with_env("cli", "TOKEN", "{{ tok }}")];
let mut vars = HashMap::new();
vars.insert("tok".to_string(), "abc123".to_string());
write_env_files(&home, &scope, &selected, &vars).unwrap();
let scoped_path = home.principal_home(&scope).env_dir().join("cli.env.json");
assert!(
scoped_path.exists(),
"env file must be under the scoped principal's home"
);
let default_path = home.principal_home(&default).env_dir().join("cli.env.json");
assert!(
!default_path.exists(),
"env file must NOT be written under `default` for a scoped principal"
);
}
#[test]
fn should_write_lock_gates_on_success() {
assert!(should_write_lock(5, 5));
assert!(should_write_lock(0, 0));
assert!(!should_write_lock(5, 3));
assert!(!should_write_lock(5, 0));
assert!(!should_write_lock(1, 0));
}
#[test]
fn partial_run_leaves_no_lock_for_retry() {
let dir = tempfile::tempdir().unwrap();
let lock_path = dir.path().join("distro.lock");
let lock = create_lock_from_parts(1, "astralis", "1.0.0", Vec::new());
let wrote = persist_lock_if_earned(&lock_path, 5, 3, &lock).unwrap();
assert!(!wrote, "partial run must not write a lock");
assert!(
load_lock(&lock_path).unwrap().is_none(),
"partial run must leave no Distro.lock on disk, else the retry is wedged"
);
let wrote = persist_lock_if_earned(&lock_path, 5, 5, &lock).unwrap();
assert!(wrote, "full success must write the lock");
assert!(
load_lock(&lock_path).unwrap().is_some(),
"full success must persist the lock"
);
}