use std::collections::BTreeMap;
use std::fs;
use lorum::config::{
LorumConfig, McpConfig, McpServer, ProjectConfig, load_config, load_project_config,
merge_configs, save_config,
};
use lorum::env_interpolate;
use lorum::sync;
#[test]
fn init_creates_config_and_add_servers() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.yaml");
let cfg = LorumConfig::default();
save_config(&config_path, &cfg).unwrap();
assert!(config_path.exists());
let mut cfg = load_config(&config_path).unwrap();
cfg.mcp.servers.insert(
"fetch".into(),
McpServer {
command: "npx".into(),
args: vec!["-y".into(), "@modelcontextprotocol/server-fetch".into()],
env: BTreeMap::new(),
},
);
cfg.mcp.servers.insert(
"memory".into(),
McpServer {
command: "npx".into(),
args: vec!["-y".into(), "@modelcontextprotocol/server-memory".into()],
env: BTreeMap::new(),
},
);
save_config(&config_path, &cfg).unwrap();
let loaded = load_config(&config_path).unwrap();
assert_eq!(loaded.mcp.servers.len(), 2);
assert!(loaded.mcp.servers.contains_key("fetch"));
assert!(loaded.mcp.servers.contains_key("memory"));
assert_eq!(
loaded.mcp.servers["fetch"].args,
vec!["-y", "@modelcontextprotocol/server-fetch"]
);
let raw = fs::read_to_string(&config_path).unwrap();
assert!(raw.contains("fetch"));
assert!(raw.contains("memory"));
assert!(raw.contains("npx"));
}
#[test]
fn sync_produces_results_for_all_adapters() {
let mcp = McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"test-server".into(),
McpServer {
command: "echo".into(),
args: vec!["hello".into()],
env: BTreeMap::new(),
},
);
m
},
};
let results = sync::sync_all(&mcp);
assert_eq!(results.len(), 9);
let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
assert!(names.contains(&"claude-code"));
assert!(names.contains(&"codex"));
assert!(names.contains(&"continue"));
assert!(names.contains(&"cursor"));
assert!(names.contains(&"proma"));
assert!(names.contains(&"kimi"));
assert!(names.contains(&"opencode"));
assert!(names.contains(&"trae"));
assert!(names.contains(&"windsurf"));
for result in &results {
if result.success {
assert_eq!(result.servers_synced, 1);
assert!(result.error.is_none());
} else {
assert!(result.error.is_some());
}
}
}
#[test]
fn full_roundtrip_add_sync_verify() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.yaml");
save_config(&config_path, &LorumConfig::default()).unwrap();
let mut cfg = load_config(&config_path).unwrap();
for (name, cmd) in [("alpha", "a"), ("beta", "b"), ("gamma", "c")] {
cfg.mcp.servers.insert(
name.into(),
McpServer {
command: cmd.into(),
args: vec![],
env: BTreeMap::new(),
},
);
}
save_config(&config_path, &cfg).unwrap();
let current = McpConfig::default();
let target = cfg.mcp.clone();
let diff = sync::compute_diff(¤t, &target);
assert_eq!(diff.added, vec!["alpha", "beta", "gamma"]);
assert!(diff.removed.is_empty());
assert!(diff.modified.is_empty());
assert!(diff.unchanged.is_empty());
assert!(!diff.is_empty());
assert_eq!(diff.change_count(), 3);
let diff_same = sync::compute_diff(&target, &target);
assert!(diff_same.added.is_empty());
assert!(diff_same.removed.is_empty());
assert!(diff_same.modified.is_empty());
assert_eq!(diff_same.unchanged.len(), 3);
assert!(diff_same.is_empty()); }
#[test]
fn backup_and_restore_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.yaml");
let original = LorumConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"original-server".into(),
McpServer {
command: "original-cmd".into(),
args: vec!["arg1".into()],
env: BTreeMap::new(),
},
);
m
},
},
..Default::default()
};
save_config(&config_path, &original).unwrap();
let backup_path = lorum::backup::create_backup("test-restore", &config_path).unwrap();
assert!(backup_path.exists());
let modified = LorumConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"modified-server".into(),
McpServer {
command: "modified-cmd".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
},
..Default::default()
};
save_config(&config_path, &modified).unwrap();
let loaded = load_config(&config_path).unwrap();
assert!(loaded.mcp.servers.contains_key("modified-server"));
assert!(!loaded.mcp.servers.contains_key("original-server"));
lorum::backup::restore_backup("test-restore", &config_path).unwrap();
let restored = load_config(&config_path).unwrap();
assert_eq!(restored, original);
}
#[test]
fn backup_cleanup_keeps_max() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.yaml");
fs::write(&config_path, "test content").unwrap();
for _ in 0..12 {
std::thread::sleep(std::time::Duration::from_millis(5));
lorum::backup::create_backup("test-cleanup", &config_path).unwrap();
}
let backups = lorum::backup::list_backups("test-cleanup").unwrap();
assert!(
backups.len() <= 10,
"expected at most 10 backups, got {}",
backups.len()
);
}
#[test]
fn global_plus_project_merge() {
let global = LorumConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"global-srv".into(),
McpServer {
command: "g".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m.insert(
"shared-srv".into(),
McpServer {
command: "global-version".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
},
..Default::default()
};
let project = ProjectConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"project-srv".into(),
McpServer {
command: "p".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m.insert(
"shared-srv".into(),
McpServer {
command: "project-version".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
},
exclude: vec![],
..Default::default()
};
let merged = merge_configs(&global, Some(&project));
assert_eq!(merged.mcp.servers.len(), 3);
assert_eq!(merged.mcp.servers["global-srv"].command, "g");
assert_eq!(merged.mcp.servers["project-srv"].command, "p");
assert_eq!(merged.mcp.servers["shared-srv"].command, "project-version");
}
#[test]
fn exclude_removes_servers() {
let global = LorumConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
for name in &["keep", "remove-me", "also-keep"] {
m.insert(
(*name).into(),
McpServer {
command: "cmd".into(),
args: vec![],
env: BTreeMap::new(),
},
);
}
m
},
},
..Default::default()
};
let project = ProjectConfig {
mcp: McpConfig::default(),
exclude: vec!["remove-me".into()],
..Default::default()
};
let merged = merge_configs(&global, Some(&project));
assert_eq!(merged.mcp.servers.len(), 2);
assert!(merged.mcp.servers.contains_key("keep"));
assert!(merged.mcp.servers.contains_key("also-keep"));
assert!(!merged.mcp.servers.contains_key("remove-me"));
}
#[test]
fn merge_no_project_returns_global() {
let global = LorumConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"only-global".into(),
McpServer {
command: "cmd".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
},
..Default::default()
};
let merged = merge_configs(&global, None);
assert_eq!(merged, global);
}
#[test]
fn project_config_save_and_load() {
let dir = tempfile::tempdir().unwrap();
let proj_dir = dir.path().join(".lorum");
fs::create_dir_all(&proj_dir).unwrap();
let config_path = proj_dir.join("config.yaml");
let yaml = r#"
mcp:
servers:
local-srv:
command: ./run.sh
args: []
env: {}
exclude:
- remote-heavy
"#;
fs::write(&config_path, yaml).unwrap();
let loaded = load_project_config(&config_path)
.unwrap()
.expect("should load project config");
assert_eq!(loaded.mcp.servers.len(), 1);
assert_eq!(loaded.mcp.servers["local-srv"].command, "./run.sh");
assert_eq!(loaded.exclude, vec!["remote-heavy"]);
}
#[test]
fn config_roundtrip_load_save() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.yaml");
let original = LorumConfig {
mcp: McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"alpha".into(),
McpServer {
command: "echo".into(),
args: vec!["alpha-arg".into()],
env: {
let mut e = BTreeMap::new();
e.insert("ALPHA_ENV".into(), "alpha-value".into());
e
},
},
);
m.insert(
"beta".into(),
McpServer {
command: "run".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
},
..Default::default()
};
save_config(&config_path, &original).unwrap();
let loaded = load_config(&config_path).unwrap();
assert_eq!(loaded, original);
}
#[test]
fn compute_diff_detects_added_removed() {
let current = McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"server-a".into(),
McpServer {
command: "a".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m.insert(
"server-b".into(),
McpServer {
command: "b".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
};
let target = McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"server-b".into(),
McpServer {
command: "b".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m.insert(
"server-c".into(),
McpServer {
command: "c".into(),
args: vec![],
env: BTreeMap::new(),
},
);
m
},
};
let diff = sync::compute_diff(¤t, &target);
assert!(diff.added.contains(&"server-c".into()));
assert!(diff.removed.contains(&"server-a".into()));
assert!(diff.unchanged.contains(&"server-b".into()));
assert!(diff.modified.is_empty());
assert_eq!(diff.change_count(), 2);
}
#[test]
fn env_interpolation_expands_vars() {
let no_expand = env_interpolate::interpolate_env("${LORUM_MISSING_VAR}", false);
assert_eq!(no_expand, "${LORUM_MISSING_VAR}");
let unset = env_interpolate::interpolate_env("${LORUM_UNSET_VAR_12345}", true);
assert_eq!(unset, "${LORUM_UNSET_VAR_12345}");
let plain = env_interpolate::interpolate_env("plain text here", true);
assert_eq!(plain, "plain text here");
let multi = env_interpolate::interpolate_env("${A} and ${B} and ${C}", true);
assert_eq!(multi, "${A} and ${B} and ${C}");
}