use std::fs;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use crate::config::profile::{
ProfileEntry, ProfileRegistry, acquire_token_lock, active_profile, base_opencrabs_dir,
create_profile, delete_profile, export_profile, hash_token, import_profile, list_profiles,
migrate_profile, release_all_locks, release_token_lock, resolve_profile_home,
set_active_profile, validate_profile_name,
};
fn fs_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn valid_profile_names() {
assert!(validate_profile_name("hermes").is_ok());
assert!(validate_profile_name("my-profile").is_ok());
assert!(validate_profile_name("test_123").is_ok());
assert!(validate_profile_name("a").is_ok());
assert!(validate_profile_name("UPPERCASE").is_ok());
assert!(validate_profile_name("MiXeD-CaSe_99").is_ok());
assert!(validate_profile_name("x".repeat(64).as_str()).is_ok());
}
#[test]
fn invalid_profile_name_default() {
let err = validate_profile_name("default").unwrap_err();
assert!(err.to_string().contains("reserved"));
}
#[test]
fn invalid_profile_name_empty() {
let err = validate_profile_name("").unwrap_err();
assert!(err.to_string().contains("1-64"));
}
#[test]
fn invalid_profile_name_too_long() {
let long = "x".repeat(65);
let err = validate_profile_name(&long).unwrap_err();
assert!(err.to_string().contains("1-64"));
}
#[test]
fn invalid_profile_name_spaces() {
let err = validate_profile_name("has spaces").unwrap_err();
assert!(err.to_string().contains("alphanumeric"));
}
#[test]
fn invalid_profile_name_slashes() {
assert!(validate_profile_name("has/slash").is_err());
assert!(validate_profile_name("back\\slash").is_err());
}
#[test]
fn invalid_profile_name_special_chars() {
assert!(validate_profile_name("name@here").is_err());
assert!(validate_profile_name("name.dot").is_err());
assert!(validate_profile_name("name!bang").is_err());
assert!(validate_profile_name("name#hash").is_err());
assert!(validate_profile_name("emoji🦀").is_err());
}
#[test]
fn validate_boundary_length_names() {
assert!(validate_profile_name("x").is_ok());
assert!(validate_profile_name(&"a".repeat(64)).is_ok());
assert!(validate_profile_name(&"a".repeat(65)).is_err());
}
#[test]
fn hash_token_deterministic() {
let h1 = hash_token("bot123:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw");
let h2 = hash_token("bot123:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw");
assert_eq!(h1, h2);
}
#[test]
fn hash_token_different_inputs() {
let h1 = hash_token("token_a");
let h2 = hash_token("token_b");
assert_ne!(h1, h2);
}
#[test]
fn hash_token_fixed_length() {
assert_eq!(hash_token("short").len(), 16);
assert_eq!(hash_token("a".repeat(1000).as_str()).len(), 16);
assert_eq!(hash_token("").len(), 16);
}
#[test]
fn hash_token_hex_chars_only() {
let h = hash_token("anything");
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hash_token_empty_string() {
let h = hash_token("");
assert_eq!(h.len(), 16);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hash_token_unicode() {
let h = hash_token("🦀🦀🦀");
assert_eq!(h.len(), 16);
}
#[test]
fn registry_default_is_empty() {
let reg = ProfileRegistry::default();
assert!(reg.profiles.is_empty());
}
#[test]
fn registry_register_single() {
let mut reg = ProfileRegistry::default();
reg.register("hermes", Some("Messenger of the gods"));
assert!(reg.profiles.contains_key("hermes"));
assert_eq!(reg.profiles["hermes"].name, "hermes");
assert_eq!(
reg.profiles["hermes"].description.as_deref(),
Some("Messenger of the gods")
);
assert!(!reg.profiles["hermes"].created_at.is_empty());
assert!(reg.profiles["hermes"].last_used.is_none());
}
#[test]
fn registry_register_no_description() {
let mut reg = ProfileRegistry::default();
reg.register("scout", None);
assert!(reg.profiles["scout"].description.is_none());
}
#[test]
fn registry_register_multiple() {
let mut reg = ProfileRegistry::default();
reg.register("alpha", Some("First"));
reg.register("beta", Some("Second"));
reg.register("gamma", None);
assert_eq!(reg.profiles.len(), 3);
}
#[test]
fn registry_register_overwrites_duplicate() {
let mut reg = ProfileRegistry::default();
reg.register("hermes", Some("v1"));
let created_v1 = reg.profiles["hermes"].created_at.clone();
std::thread::sleep(std::time::Duration::from_millis(1));
reg.register("hermes", Some("v2"));
assert_eq!(reg.profiles["hermes"].description.as_deref(), Some("v2"));
assert_ne!(reg.profiles["hermes"].created_at, created_v1);
}
#[test]
fn registry_touch_updates_last_used() {
let mut reg = ProfileRegistry::default();
reg.register("hermes", None);
assert!(reg.profiles["hermes"].last_used.is_none());
reg.touch("hermes");
assert!(reg.profiles["hermes"].last_used.is_some());
}
#[test]
fn registry_touch_nonexistent_is_noop() {
let mut reg = ProfileRegistry::default();
reg.touch("ghost");
assert!(reg.profiles.is_empty());
}
#[test]
fn registry_serde_roundtrip() {
let mut reg = ProfileRegistry::default();
reg.register("hermes", Some("Test profile"));
reg.register("scout", None);
reg.touch("hermes");
let serialized = toml::to_string_pretty(®).unwrap();
let deserialized: ProfileRegistry = toml::from_str(&serialized).unwrap();
assert_eq!(deserialized.profiles.len(), 2);
assert!(deserialized.profiles.contains_key("hermes"));
assert!(deserialized.profiles.contains_key("scout"));
assert!(deserialized.profiles["hermes"].last_used.is_some());
assert_eq!(
deserialized.profiles["hermes"].description.as_deref(),
Some("Test profile")
);
}
#[test]
fn registry_serde_empty() {
let reg = ProfileRegistry::default();
let serialized = toml::to_string_pretty(®).unwrap();
let deserialized: ProfileRegistry = toml::from_str(&serialized).unwrap();
assert!(deserialized.profiles.is_empty());
}
#[test]
fn registry_deserialized_from_toml_string() {
let toml_str = r#"
[profiles.hermes]
name = "hermes"
description = "Messenger"
created_at = "2026-03-31T00:00:00Z"
[profiles.scout]
name = "scout"
created_at = "2026-03-31T00:00:00Z"
"#;
let reg: ProfileRegistry = toml::from_str(toml_str).unwrap();
assert_eq!(reg.profiles.len(), 2);
assert_eq!(
reg.profiles["hermes"].description.as_deref(),
Some("Messenger")
);
assert!(reg.profiles["scout"].description.is_none());
}
#[test]
fn profile_entry_json_roundtrip() {
let entry = ProfileEntry {
name: "test".to_string(),
description: Some("desc".to_string()),
created_at: "2026-03-31T00:00:00Z".to_string(),
last_used: Some("2026-03-31T01:00:00Z".to_string()),
};
let json = serde_json::to_string(&entry).unwrap();
let deserialized: ProfileEntry = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "test");
assert_eq!(deserialized.description.as_deref(), Some("desc"));
assert_eq!(
deserialized.last_used.as_deref(),
Some("2026-03-31T01:00:00Z")
);
}
#[test]
fn profile_entry_optional_fields() {
let entry = ProfileEntry {
name: "minimal".to_string(),
description: None,
created_at: "2026-01-01T00:00:00Z".to_string(),
last_used: None,
};
assert!(entry.description.is_none());
assert!(entry.last_used.is_none());
}
#[test]
fn base_dir_ends_with_opencrabs() {
let base = base_opencrabs_dir();
assert!(base.ends_with(".opencrabs"));
}
#[test]
fn base_dir_is_absolute() {
let base = base_opencrabs_dir();
assert!(base.is_absolute());
}
#[test]
fn delete_default_profile_fails() {
let err = delete_profile("default").unwrap_err();
assert!(err.to_string().contains("cannot delete"));
}
#[test]
fn delete_nonexistent_profile_fails() {
let err = delete_profile("_nonexistent_profile_xyz").unwrap_err();
assert!(err.to_string().contains("does not exist"));
}
#[test]
fn export_nonexistent_profile_fails() {
let archive = std::env::temp_dir().join("_test_nonexistent_export.tar.gz");
let err = export_profile("_definitely_not_a_profile", &archive).unwrap_err();
assert!(err.to_string().contains("does not exist"));
}
#[test]
fn import_nonexistent_archive_fails() {
let err = import_profile(&PathBuf::from("/tmp/_nonexistent_archive.tar.gz")).unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn registry_load_from_real_path() {
let _guard = fs_lock();
let loaded = ProfileRegistry::load().unwrap_or_default();
let _ = loaded.profiles.len();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn list_profiles_always_includes_default() {
let _guard = fs_lock();
let profiles = list_profiles().unwrap();
assert!(!profiles.is_empty());
assert_eq!(profiles[0].name, "default");
assert!(
profiles[0]
.description
.as_deref()
.unwrap()
.contains("Default")
);
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn filesystem_operations_sequential() {
let _guard = fs_lock();
let pid = std::process::id();
let lock_dir = base_opencrabs_dir().join("locks");
fs::create_dir_all(&lock_dir).unwrap();
let name = "_test_fs_seq";
let profile_dir = base_opencrabs_dir().join("profiles").join(name);
let _ = fs::remove_dir_all(&profile_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name);
let _ = reg.save();
let path = create_profile(name, Some("sequential test")).unwrap();
assert!(path.exists(), "profile directory should be created");
assert!(path.join("memory").exists(), "memory subdir should exist");
assert!(path.join("logs").exists(), "logs subdir should exist");
let reg = ProfileRegistry::load().unwrap_or_default();
assert!(reg.profiles.contains_key(name), "should be in registry");
let err = create_profile(name, None).unwrap_err();
assert!(err.to_string().contains("already exists"));
let profiles = list_profiles().unwrap();
let found = profiles.iter().any(|p| p.name == name);
assert!(found, "should appear in list");
delete_profile(name).unwrap();
assert!(!path.exists(), "directory gone after delete");
let reg = ProfileRegistry::load().unwrap_or_default();
assert!(!reg.profiles.contains_key(name), "removed from registry");
let err = delete_profile(name).unwrap_err();
assert!(err.to_string().contains("does not exist"));
let exp_name = "_test_fs_exp";
let exp_dir = base_opencrabs_dir().join("profiles").join(exp_name);
let archive = std::env::temp_dir().join(format!("_test_fs_export_{}.tar.gz", pid));
let _ = fs::remove_dir_all(&exp_dir);
let _ = fs::remove_file(&archive);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(exp_name);
let _ = reg.save();
let dir = create_profile(exp_name, Some("export test")).unwrap();
fs::write(dir.join("config.toml"), "[agent]\ncontext_limit = 42000").unwrap();
fs::write(dir.join("memory").join("note.md"), "remember this").unwrap();
export_profile(exp_name, &archive).unwrap();
assert!(archive.exists(), "archive created");
assert!(archive.metadata().unwrap().len() > 0, "archive non-empty");
delete_profile(exp_name).unwrap();
assert!(!dir.exists());
let imported = import_profile(&archive).unwrap();
assert_eq!(imported, exp_name);
let reimported = base_opencrabs_dir().join("profiles").join(exp_name);
assert!(reimported.exists());
let config = fs::read_to_string(reimported.join("config.toml")).unwrap();
assert!(config.contains("context_limit = 42000"));
let note = fs::read_to_string(reimported.join("memory").join("note.md")).unwrap();
assert_eq!(note, "remember this");
let reg = ProfileRegistry::load().unwrap_or_default();
assert!(reg.profiles.contains_key(exp_name));
let _ = delete_profile(exp_name);
let _ = fs::remove_file(&archive);
let default_archive =
std::env::temp_dir().join(format!("_test_fs_default_export_{}.tar.gz", pid));
let _ = fs::remove_file(&default_archive);
let result = export_profile("default", &default_archive)
.or_else(|_| export_profile("default", &default_archive));
if result.is_ok() {
assert!(default_archive.exists());
}
let _ = fs::remove_file(&default_archive);
let ch1 = "_test_fs_lk1";
let th1 = hash_token("fs_lock_1");
release_token_lock(ch1, &th1);
acquire_token_lock(ch1, &th1).unwrap();
let lf1 = lock_dir.join(format!("{}_{}.lock", ch1, th1));
assert!(lf1.exists(), "lock file created");
let contents = fs::read_to_string(&lf1).unwrap();
assert!(contents.contains(&pid.to_string()), "contains our PID");
let parts: Vec<&str> = contents.splitn(2, ':').collect();
assert_eq!(parts.len(), 2, "lock file should be 'profile:pid' format");
assert!(!parts[0].is_empty(), "profile name should not be empty");
assert_eq!(parts[1], pid.to_string());
release_token_lock(ch1, &th1);
assert!(!lf1.exists(), "lock file removed after release");
let ch2 = "_test_fs_lk2";
let th2 = hash_token("fs_lock_2");
release_token_lock(ch2, &th2);
acquire_token_lock(ch2, &th2).unwrap();
acquire_token_lock(ch2, &th2).unwrap(); release_token_lock(ch2, &th2);
let ch3 = "_test_fs_lk3";
let th3 = hash_token("fs_lock_3");
let stale = lock_dir.join(format!("{}_{}.lock", ch3, th3));
fs::write(&stale, "default:999999999").unwrap();
acquire_token_lock(ch3, &th3).unwrap();
let contents = fs::read_to_string(&stale).unwrap();
assert!(contents.contains(&pid.to_string()));
release_token_lock(ch3, &th3);
let th_multi = hash_token("multi_ch");
let ch_a = "_test_fs_mca";
let ch_b = "_test_fs_mcb";
release_token_lock(ch_a, &th_multi);
release_token_lock(ch_b, &th_multi);
acquire_token_lock(ch_a, &th_multi).unwrap();
acquire_token_lock(ch_b, &th_multi).unwrap();
let la = lock_dir.join(format!("{}_{}.lock", ch_a, th_multi));
let lb = lock_dir.join(format!("{}_{}.lock", ch_b, th_multi));
assert!(la.exists());
assert!(lb.exists());
release_all_locks();
assert!(!la.exists(), "release_all cleaned lock a");
assert!(!lb.exists(), "release_all cleaned lock b");
let th_other = hash_token("other_profile_tok");
let other_lock = lock_dir.join(format!("_test_fs_other_{}.lock", th_other));
fs::write(&other_lock, "other_profile:999999999").unwrap();
release_all_locks();
assert!(other_lock.exists(), "other profile's lock preserved");
let _ = fs::remove_file(&other_lock);
release_token_lock("_nonexistent_channel", "0000000000000000");
}
#[test]
fn migrate_same_profile_errors() {
let err = migrate_profile("default", "default", false);
assert!(err.is_err());
assert!(
err.unwrap_err()
.to_string()
.contains("source and destination profiles are the same")
);
}
#[test]
fn migrate_nonexistent_source_errors() {
let err = migrate_profile("_test_migrate_no_src", "default", false);
assert!(err.is_err());
assert!(err.unwrap_err().to_string().contains("does not exist"));
}
#[test]
fn migrate_nonexistent_destination_errors() {
let err = migrate_profile("default", "_test_migrate_no_dst", false);
assert!(err.is_err());
assert!(err.unwrap_err().to_string().contains("does not exist"));
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn migrate_profile_copies_md_and_toml_files() {
let _guard = fs_lock();
let base = crate::config::profile::base_opencrabs_dir();
let src_name = "_test_migrate_src";
let dst_name = "_test_migrate_dst";
let src_dir = base.join("profiles").join(src_name);
let dst_dir = base.join("profiles").join(dst_name);
let _ = fs::remove_dir_all(&src_dir);
let _ = fs::remove_dir_all(&dst_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(src_name);
reg.profiles.remove(dst_name);
let _ = reg.save();
create_profile(src_name, Some("source")).unwrap();
create_profile(dst_name, Some("destination")).unwrap();
fs::write(src_dir.join("SOUL.md"), "# Source Soul").unwrap();
fs::write(src_dir.join("IDENTITY.md"), "# Source Identity").unwrap();
fs::write(src_dir.join("config.toml"), "[general]\nname = \"source\"").unwrap();
fs::write(src_dir.join("keys.toml"), "[keys]\nsecret = \"abc\"").unwrap();
fs::create_dir_all(src_dir.join("memory")).unwrap();
fs::write(src_dir.join("memory").join("note.md"), "# A memory").unwrap();
fs::write(src_dir.join("layout.json"), "{}").unwrap();
fs::write(src_dir.join("profiles.toml"), "skip").unwrap();
fs::write(src_dir.join("random.txt"), "not a toml or md").unwrap();
let migrated = migrate_profile(src_name, dst_name, false).unwrap();
assert!(dst_dir.join("SOUL.md").exists());
assert!(dst_dir.join("IDENTITY.md").exists());
assert!(dst_dir.join("config.toml").exists());
assert!(dst_dir.join("keys.toml").exists());
assert!(dst_dir.join("memory").join("note.md").exists());
assert_eq!(
fs::read_to_string(dst_dir.join("SOUL.md")).unwrap(),
"# Source Soul"
);
assert_eq!(
fs::read_to_string(dst_dir.join("memory").join("note.md")).unwrap(),
"# A memory"
);
assert!(!dst_dir.join("layout.json").exists());
assert!(!dst_dir.join("random.txt").exists());
assert!(!dst_dir.join("profiles.toml").exists());
assert!(migrated.contains(&"SOUL.md".to_string()));
assert!(migrated.contains(&"IDENTITY.md".to_string()));
assert!(migrated.contains(&"config.toml".to_string()));
assert!(migrated.contains(&"keys.toml".to_string()));
assert!(migrated.contains(&"memory/note.md".to_string()));
assert_eq!(migrated.len(), 5);
let _ = fs::remove_dir_all(&src_dir);
let _ = fs::remove_dir_all(&dst_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(src_name);
reg.profiles.remove(dst_name);
let _ = reg.save();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn migrate_profile_skips_existing_without_force() {
let _guard = fs_lock();
let base = crate::config::profile::base_opencrabs_dir();
let src_name = "_test_migrate_skip_src";
let dst_name = "_test_migrate_skip_dst";
let src_dir = base.join("profiles").join(src_name);
let dst_dir = base.join("profiles").join(dst_name);
let _ = fs::remove_dir_all(&src_dir);
let _ = fs::remove_dir_all(&dst_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(src_name);
reg.profiles.remove(dst_name);
let _ = reg.save();
create_profile(src_name, None).unwrap();
create_profile(dst_name, None).unwrap();
fs::write(src_dir.join("SOUL.md"), "source content").unwrap();
fs::write(dst_dir.join("SOUL.md"), "existing content").unwrap();
let migrated = migrate_profile(src_name, dst_name, false).unwrap();
assert!(
migrated.is_empty(),
"should skip existing files without --force"
);
assert_eq!(
fs::read_to_string(dst_dir.join("SOUL.md")).unwrap(),
"existing content",
"original content preserved"
);
let migrated = migrate_profile(src_name, dst_name, true).unwrap();
assert_eq!(migrated.len(), 1);
assert_eq!(
fs::read_to_string(dst_dir.join("SOUL.md")).unwrap(),
"source content",
"overwritten with source"
);
let _ = fs::remove_dir_all(&src_dir);
let _ = fs::remove_dir_all(&dst_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(src_name);
reg.profiles.remove(dst_name);
let _ = reg.save();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn migrate_from_default_profile_works() {
let _guard = fs_lock();
let base = crate::config::profile::base_opencrabs_dir();
let dst_name = "_test_migrate_from_default";
let dst_dir = base.join("profiles").join(dst_name);
let _ = fs::remove_dir_all(&dst_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(dst_name);
let _ = reg.save();
create_profile(dst_name, None).unwrap();
let result = migrate_profile("default", dst_name, false);
assert!(result.is_ok(), "migrate from default should succeed");
let _ = fs::remove_dir_all(&dst_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(dst_name);
let _ = reg.save();
}
#[test]
fn resolve_profile_home_default_is_base_dir() {
let base = base_opencrabs_dir();
let home = resolve_profile_home();
assert!(
home == base || home.starts_with(base.join("profiles")),
"home {:?} should be base {:?} or a profiles subdir",
home,
base
);
}
#[test]
fn resolve_profile_home_env_var_override() {
let base = base_opencrabs_dir();
let expected_hermes = base.join("profiles").join("hermes");
assert!(expected_hermes.ends_with("profiles/hermes"));
assert!(expected_hermes.starts_with(&base));
}
#[test]
fn active_profile_returns_none_or_valid() {
match active_profile() {
None => {} Some(name) => {
assert!(!name.is_empty(), "active profile name should not be empty");
assert!(
validate_profile_name(name).is_ok(),
"active profile name should be valid"
);
}
}
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn profiles_have_completely_separate_directories() {
let _guard = fs_lock();
let base = base_opencrabs_dir();
let name_a = "_test_iso_alpha";
let name_b = "_test_iso_beta";
let dir_a = base.join("profiles").join(name_a);
let dir_b = base.join("profiles").join(name_b);
let _ = fs::remove_dir_all(&dir_a);
let _ = fs::remove_dir_all(&dir_b);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name_a);
reg.profiles.remove(name_b);
let _ = reg.save();
create_profile(name_a, Some("Alpha")).unwrap();
create_profile(name_b, Some("Beta")).unwrap();
fs::write(dir_a.join("SOUL.md"), "I am Alpha").unwrap();
fs::write(dir_b.join("SOUL.md"), "I am Beta").unwrap();
fs::write(dir_a.join("config.toml"), "[general]\nname = \"alpha\"").unwrap();
fs::write(dir_b.join("config.toml"), "[general]\nname = \"beta\"").unwrap();
fs::create_dir_all(dir_a.join("memory")).unwrap();
fs::create_dir_all(dir_b.join("memory")).unwrap();
fs::write(dir_a.join("memory").join("fact.md"), "alpha fact").unwrap();
fs::write(dir_b.join("memory").join("fact.md"), "beta fact").unwrap();
assert_eq!(
fs::read_to_string(dir_a.join("SOUL.md")).unwrap(),
"I am Alpha"
);
assert_eq!(
fs::read_to_string(dir_b.join("SOUL.md")).unwrap(),
"I am Beta"
);
assert_eq!(
fs::read_to_string(dir_a.join("config.toml")).unwrap(),
"[general]\nname = \"alpha\""
);
assert_eq!(
fs::read_to_string(dir_b.join("config.toml")).unwrap(),
"[general]\nname = \"beta\""
);
assert_eq!(
fs::read_to_string(dir_a.join("memory/fact.md")).unwrap(),
"alpha fact"
);
assert_eq!(
fs::read_to_string(dir_b.join("memory/fact.md")).unwrap(),
"beta fact"
);
fs::write(dir_a.join("SOUL.md"), "Alpha changed").unwrap();
assert_eq!(
fs::read_to_string(dir_b.join("SOUL.md")).unwrap(),
"I am Beta"
);
delete_profile(name_a).unwrap();
assert!(!dir_a.exists());
assert!(dir_b.exists());
assert_eq!(
fs::read_to_string(dir_b.join("SOUL.md")).unwrap(),
"I am Beta"
);
let _ = fs::remove_dir_all(&dir_b);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name_a);
reg.profiles.remove(name_b);
let _ = reg.save();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn token_lock_prevents_same_token_reuse() {
let _guard = fs_lock();
let base = base_opencrabs_dir();
let locks_dir = base.join("locks");
let channel = "_test_iso_telegram";
let token_hash = hash_token("shared-bot-token-12345");
release_token_lock(channel, &token_hash);
let result = acquire_token_lock(channel, &token_hash);
assert!(result.is_ok(), "should acquire the lock");
let lock_path = locks_dir.join(format!("{}_{}.lock", channel, token_hash));
assert!(lock_path.exists());
let content = fs::read_to_string(&lock_path).unwrap();
let current_profile = active_profile().unwrap_or("default");
assert!(
content.contains(current_profile),
"lock should contain profile name"
);
assert!(
content.contains(&std::process::id().to_string()),
"lock should contain PID"
);
let result = acquire_token_lock(channel, &token_hash);
assert!(result.is_ok(), "same profile+PID should re-acquire");
let our_pid = std::process::id();
fs::write(&lock_path, format!("foreign_profile:{our_pid}")).unwrap();
let result = acquire_token_lock(channel, &token_hash);
if let Err(e) = result {
let err_msg = e.to_string();
assert!(
err_msg.contains("foreign_profile"),
"error should mention blocking profile: {}",
err_msg
);
} else {
eprintln!(
"NOTE: is_pid_alive({}) returned false on this platform, stale-lock path taken",
our_pid
);
}
fs::write(&lock_path, "dead_profile:999999").unwrap();
let result = acquire_token_lock(channel, &token_hash);
assert!(result.is_ok(), "should acquire — stale lock from dead PID");
release_token_lock(channel, &token_hash);
}
#[test]
fn different_tokens_same_channel_no_conflict() {
let _guard = fs_lock();
let channel = "_test_iso_noconflict";
let hash_a = hash_token("bot-token-aaa");
let hash_b = hash_token("bot-token-bbb");
release_token_lock(channel, &hash_a);
release_token_lock(channel, &hash_b);
assert!(acquire_token_lock(channel, &hash_a).is_ok());
assert!(acquire_token_lock(channel, &hash_b).is_ok());
let base = base_opencrabs_dir();
let locks_dir = base.join("locks");
assert!(
locks_dir
.join(format!("{}_{}.lock", channel, hash_a))
.exists()
);
assert!(
locks_dir
.join(format!("{}_{}.lock", channel, hash_b))
.exists()
);
release_token_lock(channel, &hash_a);
release_token_lock(channel, &hash_b);
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn default_profile_isolation_from_named_profiles() {
let _guard = fs_lock();
let base = base_opencrabs_dir();
let named = "_test_iso_vs_default";
let named_dir = base.join("profiles").join(named);
let _ = fs::remove_dir_all(&named_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(named);
let _ = reg.save();
create_profile(named, Some("isolated from default")).unwrap();
assert!(named_dir.exists());
assert_ne!(named_dir, base, "named profile dir must differ from base");
fs::write(named_dir.join("IDENTITY.md"), "# Named Profile").unwrap();
if base.join("IDENTITY.md").exists() {
let default_content = fs::read_to_string(base.join("IDENTITY.md")).unwrap();
assert_ne!(default_content, "# Named Profile");
}
let _ = fs::remove_dir_all(&named_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(named);
let _ = reg.save();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn concurrent_writes_to_separate_profiles_are_isolated() {
use std::thread;
let _lock = fs_lock();
let base = base_opencrabs_dir();
let name_a = "_test_conc_alpha";
let name_b = "_test_conc_beta";
let dir_a = base.join("profiles").join(name_a);
let dir_b = base.join("profiles").join(name_b);
let _ = delete_profile(name_a);
let _ = delete_profile(name_b);
create_profile(name_a, Some("concurrent alpha")).unwrap();
create_profile(name_b, Some("concurrent beta")).unwrap();
let dir_a_clone = dir_a.clone();
let dir_b_clone = dir_b.clone();
let handle_a = thread::spawn(move || {
for i in 0..50 {
let content = format!("alpha iteration {}", i);
fs::write(dir_a_clone.join("SOUL.md"), &content).unwrap();
fs::write(
dir_a_clone.join("config.toml"),
format!("[gen]\niter = {}", i),
)
.unwrap();
}
fs::read_to_string(dir_a_clone.join("SOUL.md")).unwrap()
});
let handle_b = thread::spawn(move || {
for i in 0..50 {
let content = format!("beta iteration {}", i);
fs::write(dir_b_clone.join("SOUL.md"), &content).unwrap();
fs::write(
dir_b_clone.join("config.toml"),
format!("[gen]\niter = {}", i),
)
.unwrap();
}
fs::read_to_string(dir_b_clone.join("SOUL.md")).unwrap()
});
let result_a = handle_a.join().unwrap();
let result_b = handle_b.join().unwrap();
assert!(result_a.starts_with("alpha"), "alpha got: {}", result_a);
assert!(result_b.starts_with("beta"), "beta got: {}", result_b);
let soul_a = fs::read_to_string(dir_a.join("SOUL.md")).unwrap();
let soul_b = fs::read_to_string(dir_b.join("SOUL.md")).unwrap();
assert!(
soul_a.contains("alpha"),
"alpha soul should contain 'alpha'"
);
assert!(soul_b.contains("beta"), "beta soul should contain 'beta'");
assert_ne!(soul_a, soul_b, "profile souls should be distinct");
let _ = fs::remove_dir_all(&dir_a);
let _ = fs::remove_dir_all(&dir_b);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name_a);
reg.profiles.remove(name_b);
let _ = reg.save();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn export_import_preserves_nested_memory_directories() {
let _guard = fs_lock();
let base = base_opencrabs_dir();
let src_name = "_test_nested_export_src";
let src_dir = base.join("profiles").join(src_name);
let export_path = std::env::temp_dir().join("_test_nested_export.tar.gz");
let _ = fs::remove_dir_all(&src_dir);
let _ = fs::remove_file(&export_path);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(src_name);
let _ = reg.save();
create_profile(src_name, Some("nested export test")).unwrap();
let deep_dir = src_dir.join("memory").join("projects").join("opencrabs");
fs::create_dir_all(&deep_dir).unwrap();
fs::write(src_dir.join("memory").join("user.md"), "# User prefs").unwrap();
fs::write(
src_dir.join("memory").join("projects").join("index.md"),
"# Projects",
)
.unwrap();
fs::write(deep_dir.join("architecture.md"), "# Architecture notes").unwrap();
fs::write(deep_dir.join("decisions.md"), "# ADRs").unwrap();
fs::write(src_dir.join("SOUL.md"), "# Deep soul").unwrap();
fs::write(src_dir.join("config.toml"), "[test]\nnested = true").unwrap();
export_profile(src_name, &export_path).unwrap();
assert!(export_path.exists());
assert!(export_path.metadata().unwrap().len() > 0);
delete_profile(src_name).unwrap();
assert!(!src_dir.exists());
let imported_name = import_profile(&export_path).unwrap();
let imported_dir = base.join("profiles").join(&imported_name);
assert!(imported_dir.exists());
assert_eq!(
fs::read_to_string(imported_dir.join("SOUL.md")).unwrap(),
"# Deep soul"
);
assert_eq!(
fs::read_to_string(imported_dir.join("memory").join("user.md")).unwrap(),
"# User prefs"
);
assert_eq!(
fs::read_to_string(
imported_dir
.join("memory")
.join("projects")
.join("index.md")
)
.unwrap(),
"# Projects"
);
assert_eq!(
fs::read_to_string(
imported_dir
.join("memory")
.join("projects")
.join("opencrabs")
.join("architecture.md")
)
.unwrap(),
"# Architecture notes"
);
assert_eq!(
fs::read_to_string(
imported_dir
.join("memory")
.join("projects")
.join("opencrabs")
.join("decisions.md")
)
.unwrap(),
"# ADRs"
);
let _ = fs::remove_dir_all(&imported_dir);
let _ = fs::remove_file(&export_path);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(src_name);
reg.profiles.remove(&imported_name);
let _ = reg.save();
}
#[test]
fn explicit_default_name_resolves_to_base_dir() {
let base = base_opencrabs_dir();
let _default_would_be = base.join("profiles").join("default");
assert!(
validate_profile_name("default").is_err(),
"\"default\" is reserved and cannot be created as a named profile"
);
}
#[test]
fn profile_directories_never_overlap() {
let base = base_opencrabs_dir();
let default_home = base.clone();
let hermes_home = base.join("profiles").join("hermes");
let scout_home = base.join("profiles").join("scout");
assert!(!hermes_home.starts_with(&scout_home));
assert!(!scout_home.starts_with(&hermes_home));
assert!(hermes_home.starts_with(base.join("profiles")));
assert!(scout_home.starts_with(base.join("profiles")));
assert!(!hermes_home.starts_with(default_home.join("config.toml")));
}
#[test]
fn test_resolve_profile_home_returns_valid_path() {
let base = base_opencrabs_dir();
let home = resolve_profile_home();
assert!(
home == base || home.starts_with(base.join("profiles")),
"home {:?} should be base {:?} or a profiles subdir",
home,
base
);
}
#[test]
fn test_set_and_get_active_profile() {
let result = set_active_profile(None);
match result {
Ok(()) => {
assert_eq!(
active_profile(),
None,
"active_profile should return None for default"
);
}
Err(_) => {
let current = active_profile();
if let Some(name) = current {
assert!(!name.is_empty(), "active profile name should not be empty");
}
}
}
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn test_concurrent_profile_writes() {
use std::thread;
let _guard = fs_lock();
let base = base_opencrabs_dir();
let names: Vec<String> = (0..5).map(|i| format!("_test_concurrent_{}", i)).collect();
for name in &names {
let dir = base.join("profiles").join(name);
let _ = fs::remove_dir_all(&dir);
}
let mut reg = ProfileRegistry::load().unwrap_or_default();
for name in &names {
reg.profiles.remove(name.as_str());
}
let _ = reg.save();
for name in &names {
create_profile(name, Some("concurrent test")).unwrap();
}
let handles: Vec<_> = names
.iter()
.map(|name| {
let dir = base.join("profiles").join(name);
let name = name.clone();
thread::spawn(move || {
for i in 0..20 {
let content = format!("{} iteration {}", name, i);
fs::write(dir.join("SOUL.md"), &content).unwrap();
}
name
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
for name in &names {
let dir = base.join("profiles").join(name);
assert!(dir.exists(), "profile directory for {} should exist", name);
assert!(
dir.join("memory").exists(),
"memory subdir for {} should exist",
name
);
let content = fs::read_to_string(dir.join("SOUL.md")).unwrap();
assert!(
content.starts_with(name),
"SOUL.md for {} should start with profile name, got: {}",
name,
content
);
}
for name in &names {
let _ = delete_profile(name);
}
let mut reg = ProfileRegistry::load().unwrap_or_default();
for name in &names {
reg.profiles.remove(name.as_str());
}
let _ = reg.save();
}
#[test]
#[ignore = "touches user's ~/.opencrabs/ — slow + serial; run with `cargo test -- --ignored`"]
fn test_export_import_nested_memory() {
let _guard = fs_lock();
let base = base_opencrabs_dir();
let name = "_test_exp_imp_nested";
let profile_dir = base.join("profiles").join(name);
let archive = std::env::temp_dir().join(format!(
"_test_exp_imp_nested_{}.tar.gz",
std::process::id()
));
let _ = fs::remove_dir_all(&profile_dir);
let _ = fs::remove_file(&archive);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name);
let _ = reg.save();
create_profile(name, Some("nested memory test")).unwrap();
let deep_dir = profile_dir.join("memory").join("sub").join("deep");
fs::create_dir_all(&deep_dir).unwrap();
fs::write(deep_dir.join("note.md"), "deeply nested content").unwrap();
export_profile(name, &archive).unwrap();
assert!(archive.exists(), "archive should be created");
delete_profile(name).unwrap();
assert!(
!profile_dir.exists(),
"profile dir should be gone after delete"
);
let imported_name = import_profile(&archive).unwrap();
assert_eq!(imported_name, name);
let reimported_dir = base.join("profiles").join(&imported_name);
let reimported_note = reimported_dir
.join("memory")
.join("sub")
.join("deep")
.join("note.md");
assert!(
reimported_note.exists(),
"nested note.md should survive export/import roundtrip"
);
assert_eq!(
fs::read_to_string(&reimported_note).unwrap(),
"deeply nested content",
"nested file content should be preserved"
);
let _ = delete_profile(name);
let _ = fs::remove_file(&archive);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name);
reg.profiles.remove(&imported_name);
let _ = reg.save();
}
#[test]
fn test_resolve_profile_home_default_explicit() {
let base = base_opencrabs_dir();
assert!(
validate_profile_name("default").is_err(),
"\"default\" should be a reserved name"
);
let would_be_wrong = base.join("profiles").join("default");
let original_env = std::env::var("OPENCRABS_PROFILE").ok();
unsafe { std::env::remove_var("OPENCRABS_PROFILE") };
let home = resolve_profile_home();
match active_profile() {
None => {
assert_eq!(
home, base,
"with no active profile, resolve_profile_home should return base dir"
);
assert_ne!(
home, would_be_wrong,
"should NOT resolve to profiles/default/"
);
}
Some(_) => {
assert_ne!(
home, would_be_wrong,
"should never resolve to profiles/default/"
);
}
}
if let Some(val) = original_env {
unsafe { std::env::set_var("OPENCRABS_PROFILE", val) };
}
}
const EXPECTED_SEEDED_BRAIN_FILES: &[&str] = &[
"SOUL.md",
"IDENTITY.md",
"USER.md",
"AGENTS.md",
"TOOLS.md",
"MEMORY.md",
"CODE.md",
"SECURITY.md",
];
#[test]
fn create_profile_seeds_all_brain_templates() {
let _guard = fs_lock();
let name = "_test_seed_brain";
let profile_dir = base_opencrabs_dir().join("profiles").join(name);
let _ = fs::remove_dir_all(&profile_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name);
let _ = reg.save();
let path = create_profile(name, Some("brain-seed test")).unwrap();
for filename in EXPECTED_SEEDED_BRAIN_FILES {
let target = path.join(filename);
assert!(
target.exists(),
"create_profile must seed {filename} (issue from #120 audit)",
);
let content = fs::read_to_string(&target).expect("seeded file readable");
assert!(
!content.trim().is_empty(),
"{filename} seeded but empty — bundled template is broken",
);
}
let _ = delete_profile(name);
}
#[test]
fn create_profile_does_not_overwrite_existing_brain_files() {
let _guard = fs_lock();
let name = "_test_seed_no_overwrite";
let profile_dir = base_opencrabs_dir().join("profiles").join(name);
let _ = fs::remove_dir_all(&profile_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name);
let _ = reg.save();
fs::create_dir_all(&profile_dir).unwrap();
fs::write(profile_dir.join("SOUL.md"), "# my custom soul").unwrap();
crate::config::profile::seed_brain_templates(&profile_dir);
assert_eq!(
fs::read_to_string(profile_dir.join("SOUL.md")).unwrap(),
"# my custom soul",
"seeder must not clobber an existing custom SOUL.md",
);
assert!(
profile_dir.join("TOOLS.md").exists(),
"absent templates must still be seeded",
);
let _ = fs::remove_dir_all(&profile_dir);
}
#[test]
fn create_profile_brain_files_are_non_trivial_size() {
let _guard = fs_lock();
let name = "_test_seed_sizes";
let profile_dir = base_opencrabs_dir().join("profiles").join(name);
let _ = fs::remove_dir_all(&profile_dir);
let mut reg = ProfileRegistry::load().unwrap_or_default();
reg.profiles.remove(name);
let _ = reg.save();
let path = create_profile(name, None).unwrap();
let tools_bytes = fs::metadata(path.join("TOOLS.md"))
.expect("TOOLS.md exists")
.len();
assert!(
tools_bytes >= 10_000,
"TOOLS.md seeded at only {tools_bytes} bytes — template path is probably wrong",
);
let security_bytes = fs::metadata(path.join("SECURITY.md"))
.expect("SECURITY.md exists")
.len();
assert!(
security_bytes >= 1_000,
"SECURITY.md seeded at only {security_bytes} bytes — template path is probably wrong",
);
let _ = delete_profile(name);
}