#[test]
fn resolve_config_path_explicit_overrides_all() {
let p = resolve_config_path(Some("/tmp/custom.toml"));
assert_eq!(p.unwrap(), std::path::PathBuf::from("/tmp/custom.toml"));
}
#[test]
fn resolve_config_path_explicit_even_if_nonexistent() {
let p = resolve_config_path(Some("/nonexistent/path/roboticus.toml"));
assert_eq!(
p.unwrap(),
std::path::PathBuf::from("/nonexistent/path/roboticus.toml")
);
}
#[test]
fn resolve_config_path_explicit_tilde_expands() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
let p = resolve_config_path(Some("~/roboticus.toml")).unwrap();
assert_eq!(p, std::path::PathBuf::from(home).join("roboticus.toml"));
}
#[test]
fn tilde_expansion_for_multimodal_knowledge_and_device_paths() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
let cfg = RoboticusConfig::from_str(
r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[multimodal]
media_dir = "~/media"
[[knowledge.sources]]
name = "local"
source_type = "filesystem"
path = "~/docs"
[devices]
identity_path = "~/.roboticus/device.json"
"#,
)
.unwrap();
assert_eq!(
cfg.multimodal.media_dir.unwrap(),
std::path::PathBuf::from(&home).join("media")
);
assert_eq!(
cfg.knowledge.sources[0].path.clone().unwrap(),
std::path::PathBuf::from(&home).join("docs")
);
assert_eq!(
cfg.devices.identity_path.unwrap(),
std::path::PathBuf::from(&home)
.join(".roboticus")
.join("device.json")
);
}
#[test]
fn knowledge_config_default() {
let cfg = KnowledgeConfig::default();
assert!(cfg.sources.is_empty());
}
#[test]
fn workspace_config_default() {
let cfg = WorkspaceConfig::default();
assert!(!cfg.soul_versioning);
assert!(!cfg.index_on_start);
assert!(!cfg.watch_for_changes);
}
#[test]
fn voice_channel_config_default() {
let cfg = VoiceChannelConfig::default();
assert!(!cfg.enabled);
assert!(cfg.stt_model.is_none());
assert!(cfg.tts_model.is_none());
assert!(cfg.tts_voice.is_none());
}
#[test]
fn validate_default_security_config_ok() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
"#;
RoboticusConfig::from_str(toml).unwrap();
}
#[test]
fn validate_allowlist_authority_exceeds_trusted_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[security]
allowlist_authority = "Creator"
trusted_authority = "Peer"
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("allowlist_authority"));
}
#[test]
fn validate_threat_ceiling_creator_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[security]
threat_caution_ceiling = "Creator"
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("threat_caution_ceiling"));
}
#[test]
fn validate_security_peer_ceiling_ok() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[security]
threat_caution_ceiling = "Peer"
"#;
RoboticusConfig::from_str(toml).unwrap();
}
#[test]
fn validate_routing_accuracy_floor_out_of_range_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[models.routing]
accuracy_floor = 1.5
"#;
assert!(RoboticusConfig::from_str(toml).is_err());
}
#[test]
fn validate_routing_canary_fraction_requires_canary_model() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[models.routing]
canary_fraction = 0.1
"#;
assert!(RoboticusConfig::from_str(toml).is_err());
}
#[test]
fn validate_routing_canary_model_must_not_be_blocked() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[models.routing]
canary_model = "ollama/qwen3:8b"
canary_fraction = 0.2
blocked_models = ["ollama/qwen3:8b"]
"#;
assert!(RoboticusConfig::from_str(toml).is_err());
}
#[test]
fn validate_routing_mode_invalid_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[models.routing]
mode = "random"
"#;
assert!(RoboticusConfig::from_str(toml).is_err());
}
#[test]
fn validate_routing_mode_heuristic_is_rejected() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[models.routing]
mode = "heuristic"
"#;
let err = RoboticusConfig::from_str(toml).expect_err("heuristic must be rejected");
assert!(format!("{err}").contains("models.routing.mode"));
}
#[test]
fn validate_deny_on_empty_allowlist_false_is_rejected() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[security]
deny_on_empty_allowlist = false
"#;
let err =
RoboticusConfig::from_str(toml).expect_err("legacy deny_on_empty flag must be rejected");
assert!(format!("{err}").contains("deny_on_empty_allowlist"));
}
#[test]
fn rewrite_legacy_paths_in_config_updates_forward_slashes() {
let dir = std::env::temp_dir().join(format!(
"roboticus-test-rewrite-fwd-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let toml_path = dir.join("roboticus.toml");
std::fs::write(
&toml_path,
r#"
[agent]
workspace = "~/.ironclad/workspace"
[database]
path = "~/.ironclad/state.db"
[skills]
skills_dir = "~/.ironclad/skills"
"#,
)
.unwrap();
rewrite_legacy_paths_in_config(&toml_path);
let result = std::fs::read_to_string(&toml_path).unwrap();
assert!(
!result.contains("/.ironclad/"),
"legacy paths should be rewritten"
);
assert!(result.contains("/.roboticus/workspace"));
assert!(result.contains("/.roboticus/state.db"));
assert!(result.contains("/.roboticus/skills"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn rewrite_legacy_paths_noop_when_no_legacy_refs() {
let dir = std::env::temp_dir().join(format!(
"roboticus-test-rewrite-noop-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let toml_path = dir.join("roboticus.toml");
let content = r#"
[database]
path = "~/.roboticus/state.db"
"#;
std::fs::write(&toml_path, content).unwrap();
rewrite_legacy_paths_in_config(&toml_path);
let result = std::fs::read_to_string(&toml_path).unwrap();
assert_eq!(result, content, "file should be unchanged");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn copy_dir_recursive_copies_nested_structure() {
let base = std::env::temp_dir().join(format!(
"roboticus-test-copydir-{}",
std::process::id()
));
let src = base.join("src");
let dst = base.join("dst");
std::fs::create_dir_all(src.join("sub")).unwrap();
std::fs::write(src.join("a.txt"), "hello").unwrap();
std::fs::write(src.join("sub/b.txt"), "world").unwrap();
copy_dir_recursive(&src, &dst).unwrap();
assert!(dst.join("a.txt").exists());
assert!(dst.join("sub/b.txt").exists());
assert_eq!(
std::fs::read_to_string(dst.join("a.txt")).unwrap(),
"hello"
);
assert_eq!(
std::fs::read_to_string(dst.join("sub/b.txt")).unwrap(),
"world"
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn migrate_removed_legacy_config_rewrites_removed_fields() {
let raw = r#"
[server]
host = "127.0.0.1"
[models]
primary = "ollama/qwen3:8b"
[models.routing]
mode = "heuristic"
[security]
deny_on_empty_allowlist = false
[circuit_breaker]
credit_cooldown_seconds = 300
"#;
let (rewritten, report) = migrate_removed_legacy_config(raw)
.expect("migration helper should succeed")
.expect("legacy config should be rewritten");
assert!(report.renamed_server_host_to_bind);
assert!(report.routing_mode_heuristic_rewritten);
assert!(report.deny_on_empty_allowlist_hardened);
assert!(report.removed_credit_cooldown_seconds);
assert!(rewritten.contains("bind = \"127.0.0.1\""));
assert!(rewritten.contains("mode = \"metascore\""));
assert!(rewritten.contains("deny_on_empty_allowlist = true"));
assert!(!rewritten.contains("credit_cooldown_seconds"));
}
#[test]
fn rewrite_legacy_paths_end_of_value_double_quote() {
let dir = std::env::temp_dir().join(format!(
"roboticus-test-eov-dq-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let toml_path = dir.join("roboticus.toml");
std::fs::write(
&toml_path,
r#"
[database]
path = "/home/user/.ironclad"
[agent]
workspace = "C:\Users\x\.ironclad"
"#,
)
.unwrap();
rewrite_legacy_paths_in_config(&toml_path);
let result = std::fs::read_to_string(&toml_path).unwrap();
assert!(
result.contains("/.roboticus\""),
"end-of-value double-quote Unix path should be rewritten"
);
assert!(
result.contains("\\.roboticus\""),
"end-of-value double-quote Windows path should be rewritten"
);
assert!(
!result.contains(".ironclad"),
"no legacy references should remain"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn rewrite_legacy_paths_end_of_value_single_quote() {
let dir = std::env::temp_dir().join(format!(
"roboticus-test-eov-sq-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let toml_path = dir.join("roboticus.toml");
std::fs::write(
&toml_path,
"[database]\npath = '/home/user/.ironclad'\n",
)
.unwrap();
rewrite_legacy_paths_in_config(&toml_path);
let result = std::fs::read_to_string(&toml_path).unwrap();
assert!(
result.contains("/.roboticus'"),
"end-of-value single-quote path should be rewritten"
);
assert!(
!result.contains(".ironclad"),
"no legacy references should remain"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn rewrite_legacy_paths_windows_forward_slash() {
let dir = std::env::temp_dir().join(format!(
"roboticus-test-winfwd-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let toml_path = dir.join("roboticus.toml");
std::fs::write(
&toml_path,
r#"
[database]
path = "C:/Users/user/.ironclad/state.db"
"#,
)
.unwrap();
rewrite_legacy_paths_in_config(&toml_path);
let result = std::fs::read_to_string(&toml_path).unwrap();
assert!(
result.contains("/.roboticus/"),
"Windows forward-slash path should be rewritten"
);
assert!(
!result.contains(".ironclad"),
"no legacy references should remain"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn rewrite_all_toml_files_recurses() {
let dir = std::env::temp_dir().join(format!(
"roboticus-test-recurse-{}",
std::process::id()
));
std::fs::create_dir_all(dir.join("plugins")).unwrap();
std::fs::write(
dir.join("roboticus.toml"),
"path = \"~/.ironclad/state.db\"\n",
)
.unwrap();
std::fs::write(
dir.join("plugins/myplugin.toml"),
"data = \"~/.ironclad/plugins/data\"\n",
)
.unwrap();
rewrite_all_toml_files(&dir);
let root = std::fs::read_to_string(dir.join("roboticus.toml")).unwrap();
let plugin = std::fs::read_to_string(dir.join("plugins/myplugin.toml")).unwrap();
assert!(
!root.contains(".ironclad"),
"root toml should be rewritten"
);
assert!(
!plugin.contains(".ironclad"),
"nested plugin toml should be rewritten"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn migration_sentinel_retry() {
let base = std::env::temp_dir().join(format!(
"roboticus-test-sentinel-{}",
std::process::id()
));
let new_dir = base.join(".roboticus");
let _legacy = base.join(".ironclad");
std::fs::create_dir_all(&new_dir).unwrap();
let sentinel = new_dir.join(".migration_pending_delete");
std::fs::write(&sentinel, "/nonexistent/path").unwrap();
migrate_legacy_data_dir(&base, &new_dir);
let content = std::fs::read_to_string(&sentinel);
if let Ok(c) = content {
assert_eq!(c.trim(), "/nonexistent/path");
}
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn ironclad_config_env_fallback() {
let result = resolve_config_path(Some("/tmp/explicit.toml"));
assert_eq!(
result.unwrap(),
std::path::PathBuf::from("/tmp/explicit.toml"),
"explicit path should always win"
);
}