use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}
fn fresh_dir(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"supercode-ux27-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run(supercode_home: &Path, fake_home: &Path, args: &[&str]) -> Output {
Command::new(bin())
.env("SUPERCODE_HOME", supercode_home)
.env("HOME", fake_home)
.env_remove("OPENROUTER_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.args(args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("failed to spawn the supercode binary")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
const CLAUDE_JSON: &str = r#"
{
"numStartups": 4,
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "shh" }
},
"hosted-docs": {
"type": "http",
"url": "https://example.com/mcp"
}
},
"projects": {}
}
"#;
const CODEX_TOML: &str = "\
[mcp_servers.filesystem]
command = \"mcp-server-filesystem\"
args = [\"/tmp\"]
";
fn seed_fixture_home() -> PathBuf {
let home = fresh_dir("fakehome");
std::fs::write(home.join(".claude.json"), CLAUDE_JSON).unwrap();
std::fs::create_dir_all(home.join(".codex")).unwrap();
std::fs::write(home.join(".codex").join("config.toml"), CODEX_TOML).unwrap();
home
}
#[test]
fn import_maps_claude_and_codex_stdio_servers_and_reports_the_http_skip() {
let sc_home = fresh_dir("sc1");
let home = seed_fixture_home();
let out = run(&sc_home, &home, &["mcp", "import"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let so = stdout(&out);
let se = stderr(&out);
assert!(so.contains("github"), "stdout: {so}");
assert!(so.contains("filesystem"), "stdout: {so}");
assert!(so.contains("imported 2 server(s)"), "stdout: {so}");
assert!(
!so.contains("env var"),
"env is imported now — no drop note expected: {so}"
);
assert!(se.contains("hosted-docs"), "stderr: {se}");
assert!(se.contains("http"), "stderr: {se}");
let out = run(&sc_home, &home, &["mcp", "list"]);
assert!(out.status.success());
let so = stdout(&out);
assert!(so.contains("github"), "stdout: {so}");
assert!(
so.contains("npx") && so.contains("@modelcontextprotocol/server-github"),
"stdout: {so}"
);
assert!(
so.contains("GITHUB_PERSONAL_ACCESS_TOKEN"),
"mcp list must surface the imported env var name: {so}"
);
assert!(so.contains("filesystem"), "stdout: {so}");
assert!(so.contains("mcp-server-filesystem"), "stdout: {so}");
assert!(!so.contains("hosted-docs"), "stdout: {so}");
let reg_path = sc_home.join("mcp.json");
let text = std::fs::read_to_string(®_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(
v["mcpServers"]["github"]["env"]["GITHUB_PERSONAL_ACCESS_TOKEN"], "shh",
"mcp.json: {text}"
);
assert!(
v["mcpServers"]["filesystem"].get("env").is_none(),
"a no-env import must not fabricate an env key: {text}"
);
}
#[test]
fn import_is_idempotent_second_run_reports_no_new_servers() {
let sc_home = fresh_dir("sc2");
let home = seed_fixture_home();
let first = run(&sc_home, &home, &["mcp", "import"]);
assert!(first.status.success());
assert!(stdout(&first).contains("imported 2 server(s)"));
let second = run(&sc_home, &home, &["mcp", "import"]);
assert!(second.status.success(), "stderr: {}", stderr(&second));
let so = stdout(&second);
assert!(
so.contains("nothing new to import"),
"second run must be a no-op, got: {so}"
);
let reg_path = sc_home.join("mcp.json");
let text = std::fs::read_to_string(®_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
let servers = v["mcpServers"].as_object().unwrap();
assert_eq!(servers.len(), 2, "registry: {text}");
}
#[test]
fn import_never_clobbers_a_pre_existing_supercode_entry() {
let sc_home = fresh_dir("sc3");
let home = seed_fixture_home();
let out = run(
&sc_home,
&home,
&["mcp", "add", "github", "my-own-github-server"],
);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let out = run(&sc_home, &home, &["mcp", "import"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let so = stdout(&out);
let se = stderr(&out);
assert!(so.contains("filesystem"), "stdout: {so}");
assert!(
se.contains("github") && se.contains("already exists"),
"stderr: {se}"
);
let out = run(&sc_home, &home, &["mcp", "list"]);
let so = stdout(&out);
assert!(
so.contains("my-own-github-server"),
"the user's own github server must survive import untouched: {so}"
);
}
#[test]
fn dry_run_previews_without_writing_anything() {
let sc_home = fresh_dir("sc4");
let home = seed_fixture_home();
let out = run(&sc_home, &home, &["mcp", "import", "--dry-run"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert!(stdout(&out).contains("dry-run: would import"));
assert!(!sc_home.join("mcp.json").exists());
let out = run(&sc_home, &home, &["mcp", "list"]);
assert!(
stdout(&out).contains("no MCP servers registered"),
"dry-run must not have imported anything: {}",
stdout(&out)
);
}
#[test]
fn legacy_no_env_mcp_json_survives_a_resave_unchanged() {
let sc_home = fresh_dir("sc6");
let home = seed_fixture_home();
std::fs::create_dir_all(&sc_home).unwrap();
let legacy =
r#"{"mcpServers":{"legacy-server":{"command":"my-legacy-cmd","args":["--flag"]}}}"#;
std::fs::write(sc_home.join("mcp.json"), legacy).unwrap();
let out = run(&sc_home, &home, &["mcp", "add", "new-server", "some-cmd"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let text = std::fs::read_to_string(sc_home.join("mcp.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(v["mcpServers"]["legacy-server"]["command"], "my-legacy-cmd");
assert!(
v["mcpServers"]["legacy-server"].get("env").is_none(),
"a legacy no-env entry must not grow a spurious `env` key on resave: {text}"
);
assert!(
v["mcpServers"]["new-server"].get("env").is_none(),
"`mcp add` without --env must not fabricate an env key either: {text}"
);
}
#[test]
fn mcp_add_with_env_flag_stores_and_lists_the_env() {
let sc_home = fresh_dir("sc7");
let home = seed_fixture_home();
let out = run(
&sc_home,
&home,
&[
"mcp",
"add",
"with-env",
"--env",
"API_TOKEN=abc123",
"some-cmd",
],
);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let text = std::fs::read_to_string(sc_home.join("mcp.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(
v["mcpServers"]["with-env"]["env"]["API_TOKEN"], "abc123",
"text: {text}"
);
let out = run(&sc_home, &home, &["mcp", "list"]);
let so = stdout(&out);
assert!(
so.contains("API_TOKEN"),
"mcp list must show the env var name: {so}"
);
}
#[test]
fn from_claude_only_ignores_codex_servers() {
let sc_home = fresh_dir("sc5");
let home = seed_fixture_home();
let out = run(&sc_home, &home, &["mcp", "import", "--from", "claude"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let so = stdout(&out);
assert!(so.contains("github"), "stdout: {so}");
assert!(
!so.contains("filesystem"),
"--from claude must not pull in the codex server: {so}"
);
}