#[test]
fn validate_empty_agent_name_fails() {
let toml = r#"
[agent]
name = ""
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("agent.name"));
}
#[test]
fn validate_empty_agent_id_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = ""
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("agent.id"));
}
#[test]
fn validate_empty_model_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = ""
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("models.primary"));
}
#[test]
fn validate_invalid_bind_address_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
bind = "not-an-ip"
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("not a valid IP"));
}
#[test]
fn validate_localhost_bind_ok() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
bind = "localhost"
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
"#;
RoboticusConfig::from_str(toml).unwrap();
}
#[test]
fn validate_invalid_session_scope_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[session]
scope_mode = "invalid"
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("scope_mode"));
}
#[test]
fn validate_group_scope_ok() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[session]
scope_mode = "group"
"#;
RoboticusConfig::from_str(toml).unwrap();
}
#[test]
fn validate_negative_minimum_reserve_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[treasury]
minimum_reserve = -1.0
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("minimum_reserve"));
}
#[test]
fn validate_zero_payment_cap_fails() {
let toml = r#"
[agent]
name = "TestBot"
id = "test"
[server]
port = 9999
[database]
path = "/tmp/test.db"
[models]
primary = "ollama/qwen3:8b"
[treasury]
per_payment_cap = 0.0
"#;
let err = RoboticusConfig::from_str(toml).unwrap_err();
assert!(err.to_string().contains("per_payment_cap"));
}
#[test]
fn startup_announcements_none_returns_empty() {
let cfg = ChannelsConfig::default();
assert!(cfg.startup_announcement_channels().is_empty());
}
#[test]
fn startup_announcements_flag_returns_empty() {
let cfg = ChannelsConfig {
startup_announcements: Some(StartupAnnouncementsConfig::Flag(true)),
..ChannelsConfig::default()
};
assert!(cfg.startup_announcement_channels().is_empty());
}
#[test]
fn startup_announcements_text_returns_normalized() {
let cfg = ChannelsConfig {
startup_announcements: Some(StartupAnnouncementsConfig::Text("Telegram".into())),
..ChannelsConfig::default()
};
assert_eq!(cfg.startup_announcement_channels(), vec!["telegram"]);
}
#[test]
fn startup_announcements_text_none_variant() {
let cfg = ChannelsConfig {
startup_announcements: Some(StartupAnnouncementsConfig::Text("none".into())),
..ChannelsConfig::default()
};
assert!(cfg.startup_announcement_channels().is_empty());
}
#[test]
fn startup_announcements_channels_dedup_and_sort() {
let cfg = ChannelsConfig {
startup_announcements: Some(StartupAnnouncementsConfig::Channels(vec![
"whatsapp".into(),
"telegram".into(),
"TELEGRAM".into(),
"none".into(),
])),
..ChannelsConfig::default()
};
let ch = cfg.startup_announcement_channels();
assert_eq!(ch, vec!["telegram", "whatsapp"]);
}
#[test]
fn expand_tilde_no_tilde() {
let p = PathBuf::from("/absolute/path");
assert_eq!(expand_tilde(&p), p);
}
#[test]
fn expand_tilde_with_tilde() {
let p = PathBuf::from("~/Documents/vault");
let expanded = expand_tilde(&p);
assert!(!expanded.to_str().unwrap().starts_with("~"));
assert!(expanded.to_str().unwrap().contains("Documents/vault"));
}
#[test]
fn provider_config_new() {
let pc = ProviderConfig::new("http://localhost:11434", "T1");
assert_eq!(pc.url, "http://localhost:11434");
assert_eq!(pc.tier, "T1");
assert!(pc.format.is_none());
assert!(pc.api_key_env.is_none());
assert!(pc.is_local.is_none());
assert!(pc.tpm_limit.is_none());
assert!(pc.rpm_limit.is_none());
}
#[test]
fn mcp_transport_default_is_stdio() {
let t = McpTransport::default();
assert!(matches!(t, McpTransport::Stdio));
}
#[test]
fn mcp_server_config_stdio_deserializes() {
let toml = r#"
name = "test-server"
enabled = true
[spec]
type = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-test"]
"#;
let cfg: McpServerConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.name, "test-server");
assert!(cfg.enabled);
assert!(matches!(cfg.spec, McpServerSpec::Stdio { .. }));
if let McpServerSpec::Stdio { command, args, .. } = &cfg.spec {
assert_eq!(command, "npx");
assert_eq!(args, &["-y", "@modelcontextprotocol/server-test"]);
}
}
#[test]
fn mcp_server_config_sse_deserializes() {
let toml = r#"
name = "remote-server"
enabled = true
[spec]
type = "sse"
url = "http://localhost:3001/sse"
"#;
let cfg: McpServerConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.name, "remote-server");
assert!(cfg.enabled);
assert!(matches!(cfg.spec, McpServerSpec::Sse { .. }));
if let McpServerSpec::Sse { url } = &cfg.spec {
assert_eq!(url, "http://localhost:3001/sse");
}
}
#[test]
fn mcp_config_servers_field_deserializes() {
let toml = r#"
server_enabled = false
server_port = 3001
[[servers]]
name = "github"
enabled = true
[servers.spec]
type = "stdio"
command = "gh"
args = ["mcp", "serve"]
"#;
let cfg: McpConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.servers.len(), 1);
assert_eq!(cfg.servers[0].name, "github");
assert!(cfg.clients.is_empty());
}
#[test]
fn mcp_config_legacy_clients_still_deserializes() {
let toml = r#"
server_enabled = false
server_port = 3001
[[clients]]
name = "legacy-server"
url = "http://localhost:3001/sse"
"#;
let cfg: McpConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.clients.len(), 1);
assert_eq!(cfg.clients[0].name, "legacy-server");
assert!(cfg.servers.is_empty());
}
#[test]
fn home_dir_returns_valid_path() {
let h = home_dir();
assert!(h.is_absolute() || h == std::path::Path::new("/tmp"));
}
#[test]
fn dirs_next_appends_roboticus() {
let d = dirs_next();
assert!(d.to_str().unwrap().contains(".roboticus"));
}