#![cfg(target_os = "macos")]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn default_paths_use_application_support_and_allow_spaces() {
let temp = test_dir("paths with spaces");
let user_home = temp.join("user home");
fs::create_dir_all(&user_home).unwrap();
let output = Command::new(bin())
.env_remove("BOT_FORGE_HOME")
.env_remove("BOT_FORGE_CONFIG_DIR")
.env("HOME", &user_home)
.env("USERPROFILE", &user_home)
.args(["doctor", "--format", "json"])
.output()
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let data = value
.as_array()
.unwrap()
.iter()
.find(|check| check["id"] == "filesystem.data-dir-permission")
.unwrap();
assert_eq!(
data["summary"],
format!(
"{} writable",
user_home
.join("Library/Application Support/bot-forge")
.display()
)
);
assert!(
user_home
.join("Library/Application Support/bot-forge")
.is_dir()
);
fs::remove_dir_all(temp).unwrap();
}
#[test]
fn home_takes_precedence_over_a_foreign_userprofile() {
let temp = test_dir("home precedence");
let user_home = temp.join("mac home");
let foreign_home = temp.join("foreign profile");
let output = Command::new(bin())
.env_remove("BOT_FORGE_HOME")
.env_remove("BOT_FORGE_CONFIG_DIR")
.env("HOME", &user_home)
.env("USERPROFILE", &foreign_home)
.args(["doctor", "--format", "json"])
.output()
.unwrap();
assert_success(&output);
assert!(
user_home
.join("Library/Application Support/bot-forge")
.is_dir()
);
assert!(!foreign_home.exists());
fs::remove_dir_all(temp).unwrap();
}
#[test]
fn doctor_reports_the_explicit_config_and_checks_clang_with_xcrun() {
let temp = test_dir("doctor config");
let config = temp.join("selected.toml");
fs::create_dir_all(&temp).unwrap();
fs::write(&config, "catalog = \"rust-dev\"\n").unwrap();
let output = Command::new(bin())
.args(["doctor", "--format", "json", "--config", text(&config)])
.output()
.unwrap();
assert_success(&output);
let checks: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let source = checks
.as_array()
.unwrap()
.iter()
.find(|check| check["id"] == "config.source")
.unwrap();
assert_eq!(source["summary"], config.display().to_string());
assert!(
checks
.as_array()
.unwrap()
.iter()
.any(|check| check["id"] == "system.macos-translation")
);
fs::remove_dir_all(temp).unwrap();
}
#[test]
fn default_catalog_selects_only_macos_backends() {
let temp = test_dir("default-plan");
let config = temp.join("bot-forge.toml");
fs::create_dir_all(&temp).unwrap();
fs::write(&config, "catalog = \"rust-dev\"\n").unwrap();
for profile in ["minimal", "standard", "advanced"] {
let output = Command::new(bin())
.args([
"plan",
profile,
"--format",
"json",
"--config",
text(&config),
])
.output()
.unwrap();
assert_success(&output);
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["target"]["os"], "macos");
let encoded = String::from_utf8(output.stdout).unwrap();
assert!(encoded.contains("macos-brew"));
assert!(!encoded.contains("linux-apt"));
assert!(!encoded.contains("windows-winget"));
}
fs::remove_dir_all(temp).unwrap();
}
#[test]
fn brew_install_is_typed_and_persists_zsh_path() {
let temp = test_dir("brew typed");
let fake_bin = temp.join("fake bin");
let home = temp.join("state with spaces");
let user_home = temp.join("user home");
fs::create_dir_all(&fake_bin).unwrap();
let log = temp.join("brew.log");
write_executable(
&fake_bin.join("brew"),
r#"#!/bin/sh
printf '%s\n' "$HOMEBREW_NO_AUTO_UPDATE|$*" >> "$BOT_FORGE_TEST_LOG"
printf '#!/bin/sh\nexit 0\n' > "$BOT_FORGE_TEST_BIN/demo-tool"
chmod +x "$BOT_FORGE_TEST_BIN/demo-tool"
"#,
);
let config = write_brew_config(&temp, "demo", "demo-formula", "demo-tool");
let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
let output = Command::new(bin())
.env("BOT_FORGE_HOME", &home)
.env("HOME", &user_home)
.env("USERPROFILE", &user_home)
.env("SHELL", "/bin/zsh")
.env("PATH", path)
.env("BOT_FORGE_TEST_LOG", &log)
.env("BOT_FORGE_TEST_BIN", &fake_bin)
.args(["install", "macos-test", "--yes", "--config", text(&config)])
.output()
.unwrap();
assert_success(&output);
assert_eq!(
fs::read_to_string(log).unwrap(),
"1|install --formula demo-formula\n"
);
let profile = fs::read_to_string(user_home.join(".zprofile")).unwrap();
assert!(profile.contains(&home.join("bin").display().to_string()));
assert!(!user_home.join(".bashrc").exists());
fs::remove_dir_all(temp).unwrap();
}
#[test]
fn brew_install_persists_fish_path_with_fish_syntax() {
let temp = test_dir("brew fish");
let fake_bin = temp.join("fake bin");
let home = temp.join("state");
let user_home = temp.join("user home");
fs::create_dir_all(&fake_bin).unwrap();
write_executable(
&fake_bin.join("brew"),
"#!/bin/sh\nprintf '#!/bin/sh\\nexit 0\\n' > \"$BOT_FORGE_TEST_BIN/demo-tool\"\nchmod +x \"$BOT_FORGE_TEST_BIN/demo-tool\"\n",
);
let config = write_brew_config(&temp, "fish-demo", "demo-formula", "demo-tool");
let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
let output = Command::new(bin())
.env("BOT_FORGE_HOME", &home)
.env("HOME", &user_home)
.env("SHELL", "/opt/homebrew/bin/fish")
.env("PATH", path)
.env("BOT_FORGE_TEST_BIN", &fake_bin)
.args(["install", "macos-test", "--yes", "--config", text(&config)])
.output()
.unwrap();
assert_success(&output);
let profile = fs::read_to_string(user_home.join(".config/fish/config.fish")).unwrap();
assert!(profile.contains("set -gx PATH"));
assert!(!profile.contains("export PATH="));
fs::remove_dir_all(temp).unwrap();
}
#[test]
fn brew_installs_are_serialized_across_processes() {
let temp = test_dir("brew-lock");
let fake_bin = temp.join("bin");
let home = temp.join("shared-home");
let log = temp.join("events.log");
fs::create_dir_all(&fake_bin).unwrap();
write_executable(
&fake_bin.join("brew"),
r#"#!/bin/sh
formula="$3"
printf 'start-%s\n' "$formula" >> "$BOT_FORGE_TEST_LOG"
sleep 1
printf '#!/bin/sh\nexit 0\n' > "$BOT_FORGE_TEST_BIN/tool-$formula"
chmod +x "$BOT_FORGE_TEST_BIN/tool-$formula"
printf 'end-%s\n' "$formula" >> "$BOT_FORGE_TEST_LOG"
"#,
);
let first = write_brew_config(&temp, "one", "one", "tool-one");
let second = write_brew_config(&temp, "two", "two", "tool-two");
let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
let launch = |config: &Path| {
let mut command = Command::new(bin());
command
.env("BOT_FORGE_HOME", &home)
.env("HOME", temp.join("user-home"))
.env("SHELL", "/bin/zsh")
.env("PATH", &path)
.env("BOT_FORGE_TEST_LOG", &log)
.env("BOT_FORGE_TEST_BIN", &fake_bin)
.args(["install", "macos-test", "--yes", "--config", text(config)]);
command.spawn().unwrap()
};
let mut left = launch(&first);
let mut right = launch(&second);
assert!(left.wait().unwrap().success());
assert!(right.wait().unwrap().success());
let events = fs::read_to_string(log).unwrap();
let lines = events.lines().collect::<Vec<_>>();
assert_eq!(lines.len(), 4, "{events}");
assert!(lines[0].starts_with("start-"), "{events}");
assert!(lines[1].starts_with("end-"), "{events}");
assert!(lines[2].starts_with("start-"), "{events}");
assert!(lines[3].starts_with("end-"), "{events}");
fs::remove_dir_all(temp).unwrap();
}
fn write_brew_config(temp: &Path, id: &str, formula: &str, program: &str) -> PathBuf {
let path = temp.join(format!("{id}.toml"));
fs::write(
&path,
format!(
r#"catalog = "rust-dev"
[profiles.macos-test]
components = ["{id}"]
[[components]]
id = "{id}"
platforms = ["macos-*"]
[components.detect]
kind = "command"
program = "{program}"
[components.install]
backend = "brew"
formulae = ["{formula}"]
"#
),
)
.unwrap();
path
}
fn write_executable(path: &Path, contents: &str) {
fs::write(path, contents).unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_bot-forge")
}
fn text(path: &Path) -> &str {
path.to_str().unwrap()
}
fn assert_success(output: &Output) {
assert!(
output.status.success(),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn test_dir(name: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!(
"bot-forge-macos-{}-{nonce}-{name}",
std::process::id()
))
}