use super::*;
#[test]
fn mcp_stdio_env_allowlist_excludes_secrets_and_is_closed() {
let allow = mcp_stdio_env_passthrough();
for secret in [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"AWS_SECRET_ACCESS_KEY",
"GITHUB_TOKEN",
"DGX_API_KEY",
"NVIDIA_API_KEY",
crate::secrets::PASSPHRASE_ENV,
] {
assert!(!allow.contains(&secret), "{secret} must never be inherited");
}
assert!(allow.contains(&"PATH"), "a child needs PATH to exec");
for base in shell_env_passthrough_default() {
assert!(
allow.contains(&base.as_str()),
"{base} (shell default) should be covered"
);
}
}
#[test]
fn with_mcp_enabled_toggles_and_preserves_comments() {
let text = "# my config\n[[mcp_servers]]\nname = \"modulex\"\ncommand = \"modulex-mcp\"\n";
let off = Config::with_mcp_enabled(text, "modulex", false).unwrap();
assert!(off.contains("enabled = false"));
assert!(off.contains("# my config"));
let on = Config::with_mcp_enabled(&off, "modulex", true).unwrap();
assert!(!on.contains("enabled"));
assert!(Config::with_mcp_enabled(text, "nope", false).is_err());
let e: crate::mcp::McpServerEntry = toml::from_str("name = \"x\"\ncommand = \"x\"\n").unwrap();
assert!(e.enabled);
let d: crate::mcp::McpServerEntry =
toml::from_str("name = \"x\"\ncommand = \"x\"\nenabled = false\n").unwrap();
assert!(!d.enabled);
}
#[test]
fn with_mcp_server_added_appends_and_preserves_comments() {
let text = "\
# hand-authored config
default_backend = \"local\" # keep me
[[mcp_servers]]
name = \"modulex\"
command = \"modulex-mcp\"
";
let entry = crate::mcp::McpServerEntry {
name: "scrybe".into(),
enabled: true,
transport: crate::mcp::TransportKind::Stdio,
command: Some("scrybe-mcp-server".into()),
args: vec!["stdio".into()],
env: std::collections::BTreeMap::from([(
"SCRYBE_LOG".to_string(),
crate::mcp::SecretValue::literal("info"),
)]),
url: None,
headers: std::collections::BTreeMap::new(),
login_argv: Vec::new(),
request_timeout_secs: Some(120),
trust: crate::mcp::McpTrust::Trusted,
origin: None,
};
let out = Config::with_mcp_server_added(text, &entry).unwrap();
assert!(
out.contains("# hand-authored config"),
"comment lost: {out}"
);
assert!(out.contains("# keep me"), "inline comment lost: {out}");
assert!(out.contains("modulex-mcp"), "existing entry lost: {out}");
let cfg: Config = toml::from_str(&out).unwrap();
assert_eq!(cfg.mcp_servers.len(), 2);
let added = cfg.mcp_servers.iter().find(|s| s.name == "scrybe").unwrap();
assert_eq!(added.command.as_deref(), Some("scrybe-mcp-server"));
assert_eq!(added.args, vec!["stdio"]);
assert_eq!(
added
.env
.get("SCRYBE_LOG")
.and_then(crate::mcp::SecretValue::as_literal),
Some("info")
);
assert_eq!(added.request_timeout_secs, Some(120));
assert!(added.enabled);
assert!(!out.contains("enabled"), "default enabled written: {out}");
assert!(!out.contains("type"), "default transport written: {out}");
}
#[test]
fn with_mcp_server_added_creates_section_in_empty_text() {
let entry = crate::mcp::McpServerEntry {
name: "fs".into(),
enabled: true,
transport: crate::mcp::TransportKind::Stdio,
command: Some("mcp-fs".into()),
args: vec![],
env: std::collections::BTreeMap::new(),
url: None,
headers: std::collections::BTreeMap::new(),
login_argv: Vec::new(),
request_timeout_secs: None,
trust: crate::mcp::McpTrust::Trusted,
origin: None,
};
let out = Config::with_mcp_server_added("", &entry).unwrap();
let cfg: Config = toml::from_str(&out).unwrap();
assert_eq!(cfg.mcp_servers.len(), 1);
assert_eq!(cfg.mcp_servers[0].name, "fs");
assert_eq!(cfg.mcp_servers[0].command.as_deref(), Some("mcp-fs"));
}
#[test]
fn with_mcp_server_added_writes_sse_transport_and_url() {
let entry = crate::mcp::McpServerEntry {
name: "remote".into(),
enabled: true,
transport: crate::mcp::TransportKind::Sse,
command: None,
args: vec![],
env: std::collections::BTreeMap::new(),
url: Some("https://mcp.example/sse".into()),
headers: std::collections::BTreeMap::new(),
login_argv: Vec::new(),
request_timeout_secs: None,
trust: crate::mcp::McpTrust::Trusted,
origin: None,
};
let out = Config::with_mcp_server_added("", &entry).unwrap();
let cfg: Config = toml::from_str(&out).unwrap();
assert_eq!(cfg.mcp_servers[0].transport, crate::mcp::TransportKind::Sse);
assert_eq!(
cfg.mcp_servers[0].url.as_deref(),
Some("https://mcp.example/sse")
);
}
#[test]
fn with_mcp_server_added_rejects_duplicates_and_invalid_entries() {
let text = "[[mcp_servers]]\nname = \"scrybe\"\ncommand = \"scrybe-mcp-server\"\n";
let dup = crate::mcp::McpServerEntry {
name: "scrybe".into(),
enabled: true,
transport: crate::mcp::TransportKind::Stdio,
command: Some("other".into()),
args: vec![],
env: std::collections::BTreeMap::new(),
url: None,
headers: std::collections::BTreeMap::new(),
login_argv: Vec::new(),
request_timeout_secs: None,
trust: crate::mcp::McpTrust::Trusted,
origin: None,
};
let err = Config::with_mcp_server_added(text, &dup).unwrap_err();
assert!(err.to_string().contains("scrybe"), "names the dup: {err}");
let no_cmd = crate::mcp::McpServerEntry {
name: "ghost".into(),
command: None,
..dup.clone()
};
assert!(Config::with_mcp_server_added("", &no_cmd).is_err());
let no_url = crate::mcp::McpServerEntry {
name: "ghost".into(),
transport: crate::mcp::TransportKind::Http,
command: None,
..dup.clone()
};
assert!(Config::with_mcp_server_added("", &no_url).is_err());
let unnamed = crate::mcp::McpServerEntry {
name: " ".into(),
..dup.clone()
};
assert!(Config::with_mcp_server_added("", &unnamed).is_err());
}
#[test]
fn with_mcp_server_removed_deletes_only_the_named_entry() {
let text = "\
# my config
[[mcp_servers]]
name = \"keep\"
command = \"keep-mcp\" # keep note
[[mcp_servers]]
name = \"drop\"
command = \"drop-mcp\"
";
let out = Config::with_mcp_server_removed(text, "drop").unwrap();
assert!(out.contains("# my config"), "comment lost: {out}");
assert!(out.contains("# keep note"), "inline comment lost: {out}");
let cfg: Config = toml::from_str(&out).unwrap();
assert_eq!(cfg.mcp_servers.len(), 1);
assert_eq!(cfg.mcp_servers[0].name, "keep");
assert!(!out.contains("drop-mcp"));
}
#[test]
fn with_mcp_server_removed_reports_a_non_array_section_accurately() {
let text = "mcp_servers = [ { name = \"x\", command = \"y\" } ]\n";
let err = Config::with_mcp_server_removed(text, "x").unwrap_err();
assert!(
err.to_string().contains("not an array of tables"),
"wrong-shape section misreported: {err}"
);
let err = Config::with_mcp_server_removed("mcp_servers = 3\n", "x").unwrap_err();
assert!(
err.to_string().contains("not an array of tables"),
"scalar section misreported: {err}"
);
}
#[test]
fn mcp_writer_error_branches_are_loud() {
let entry = crate::mcp::McpServerEntry {
name: "x".into(),
enabled: true,
transport: crate::mcp::TransportKind::Stdio,
command: Some("x-mcp".into()),
args: vec![],
env: std::collections::BTreeMap::new(),
url: None,
headers: std::collections::BTreeMap::new(),
login_argv: Vec::new(),
request_timeout_secs: None,
trust: crate::mcp::McpTrust::Trusted,
origin: None,
};
let err = Config::with_mcp_server_added("not toml [", &entry).unwrap_err();
assert!(err.to_string().contains("not valid TOML"), "{err}");
let err = Config::with_mcp_server_removed("not toml [", "x").unwrap_err();
assert!(err.to_string().contains("not valid TOML"), "{err}");
let err = Config::with_mcp_server_added("mcp_servers = 3\n", &entry).unwrap_err();
assert!(err.to_string().contains("not an array of tables"), "{err}");
let oversized = crate::mcp::McpServerEntry {
login_argv: Vec::new(),
request_timeout_secs: Some(u64::MAX),
..entry
};
let err = Config::with_mcp_server_added("", &oversized).unwrap_err();
assert!(err.to_string().contains("out of range"), "{err}");
}
#[test]
fn with_mcp_server_removed_errors_when_absent() {
let text = "[[mcp_servers]]\nname = \"present\"\ncommand = \"x\"\n";
let err = Config::with_mcp_server_removed(text, "ghost").unwrap_err();
assert!(err.to_string().contains("ghost"), "names the miss: {err}");
assert!(Config::with_mcp_server_removed("", "ghost").is_err());
}