use super::*;
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
super::env_test_lock()
}
#[test]
fn managed_claude_config_dir_nests_under_trusty_tools() {
let base = PathBuf::from("/home/bob");
assert_eq!(
managed_claude_config_dir_at(&base),
PathBuf::from("/home/bob/.trusty-tools/trusty-mpm/claude-config")
);
if let Some(dir) = managed_claude_config_dir() {
assert!(
dir.ends_with("trusty-mpm/claude-config"),
"managed config dir must end with trusty-mpm/claude-config, got {}",
dir.display()
);
assert!(
dir.to_string_lossy().contains(".trusty-tools"),
"managed config dir must live under .trusty-tools, got {}",
dir.display()
);
}
}
#[test]
fn default_template_is_trusty_mpm_projects() {
let _g = env_lock();
unsafe { std::env::remove_var(WORKSPACE_ROOT_ENV) };
let root = workspace_root(&TrustyToolsConfig::default());
assert!(
root.ends_with(DEFAULT_WORKSPACE_DIR),
"expected …/{DEFAULT_WORKSPACE_DIR}, got {}",
root.display()
);
}
#[test]
fn env_overrides_config_and_default() {
let _g = env_lock();
let cfg = TrustyToolsConfig {
workspace_root_template: Some("~/from-config".into()),
..Default::default()
};
unsafe { std::env::set_var(WORKSPACE_ROOT_ENV, "/explicit/env/root") };
let root = workspace_root(&cfg);
unsafe { std::env::remove_var(WORKSPACE_ROOT_ENV) };
assert_eq!(root, PathBuf::from("/explicit/env/root"));
}
#[test]
fn config_template_used_when_no_env() {
let _g = env_lock();
unsafe { std::env::remove_var(WORKSPACE_ROOT_ENV) };
let cfg = TrustyToolsConfig {
workspace_root_template: Some("/custom/projects".into()),
..Default::default()
};
let root = workspace_root(&cfg);
assert_eq!(root, PathBuf::from("/custom/projects"));
}
#[test]
fn worktrees_dirname_defaults_to_dot_worktrees() {
let _g = env_lock();
unsafe { std::env::remove_var(WORKTREES_DIRNAME_ENV) };
let cfg = TrustyToolsConfig::default();
assert_eq!(worktrees_dirname(&cfg), DEFAULT_WORKTREES_DIRNAME);
assert_eq!(worktrees_dirname(&cfg), ".worktrees");
}
#[test]
fn worktrees_dirname_honours_config() {
let _g = env_lock();
unsafe { std::env::remove_var(WORKTREES_DIRNAME_ENV) };
let cfg = TrustyToolsConfig {
worktrees_dirname: Some(".sessions".into()),
..Default::default()
};
assert_eq!(worktrees_dirname(&cfg), ".sessions");
unsafe { std::env::set_var(WORKTREES_DIRNAME_ENV, ".from-env") };
let observed_env = worktrees_dirname(&cfg);
unsafe { std::env::remove_var(WORKTREES_DIRNAME_ENV) };
assert_eq!(
observed_env, ".from-env",
"env must outrank the config value"
);
let reserved = TrustyToolsConfig {
worktrees_dirname: Some("worktrees".into()),
..Default::default()
};
assert_eq!(
worktrees_dirname(&reserved),
DEFAULT_WORKTREES_DIRNAME,
"a reserved name must be refused even when explicitly configured"
);
}
#[test]
fn tilde_expansion() {
use trusty_common::workspace_layout::expand_tilde;
let home = PathBuf::from("/home/bob");
assert_eq!(
expand_tilde("~/trusty-mpm-projects", &home),
PathBuf::from("/home/bob/trusty-mpm-projects")
);
assert_eq!(expand_tilde("~", &home), home);
assert_eq!(expand_tilde("/abs/path", &home), PathBuf::from("/abs/path"));
}
#[test]
fn github_config_yaml_round_trip() {
let cfg = TrustyToolsConfig {
github: Some(GithubConfig {
config_dir: Some(PathBuf::from("/home/bob/.config/gh-work")),
token_env: Some("WORK_GH_TOKEN".into()),
account: Some("bob-work".into()),
host: Some("github.example.com".into()),
}),
..Default::default()
};
let yaml = serde_yaml::to_string(&cfg).expect("serialise");
let back: TrustyToolsConfig = serde_yaml::from_str(&yaml).expect("deserialise");
assert_eq!(cfg, back);
assert!(!yaml.contains("workspace_root_template"), "yaml: {yaml}");
assert!(yaml.contains("github:"), "yaml: {yaml}");
}
#[test]
fn github_config_stores_only_env_name() {
let cfg = GithubConfig {
token_env: Some("MY_GH_TOKEN".into()),
..Default::default()
};
let yaml = serde_yaml::to_string(&cfg).expect("serialise");
assert!(yaml.contains("token_env"), "yaml: {yaml}");
assert!(yaml.contains("MY_GH_TOKEN"), "yaml: {yaml}");
assert!(
!yaml.contains("token:"),
"must not have a bare token field: {yaml}"
);
}
#[test]
fn project_config_gh_user_yaml_round_trip() {
let cfg = TrustyToolsConfig {
projects: vec![ProjectConfig {
name: "trusty-tools".into(),
repo_url: "https://github.com/bobmatnyc/trusty-tools".into(),
default_branch: Some("main".into()),
stack_hint: None,
tags: None,
description: None,
gh_user: Some("bobmatnyc".into()),
gh_account: Some("bobmatnyc".into()),
github: None,
commit_name: None,
commit_email: None,
untracked_sync: None,
worktree: None,
}],
..Default::default()
};
let yaml = serde_yaml::to_string(&cfg).expect("serialise");
assert!(yaml.contains("gh_user"), "yaml: {yaml}");
assert!(yaml.contains("gh_account"), "yaml: {yaml}");
let back: TrustyToolsConfig = serde_yaml::from_str(&yaml).expect("deserialise");
assert_eq!(cfg, back);
let no_pref = TrustyToolsConfig {
projects: vec![ProjectConfig {
name: "other".into(),
repo_url: "https://github.com/o/other".into(),
default_branch: None,
stack_hint: None,
tags: None,
description: None,
gh_user: None,
gh_account: None,
github: None,
commit_name: None,
commit_email: None,
untracked_sync: None,
worktree: None,
}],
..Default::default()
};
let yaml_no_pref = serde_yaml::to_string(&no_pref).expect("serialise");
assert!(!yaml_no_pref.contains("gh_user"), "yaml: {yaml_no_pref}");
assert!(!yaml_no_pref.contains("gh_account"), "yaml: {yaml_no_pref}");
}
#[test]
fn project_config_github_and_commit_identity_yaml_round_trip() {
let cfg = TrustyToolsConfig {
projects: vec![ProjectConfig {
name: "work-repo".into(),
repo_url: "https://github.com/acme/work-repo".into(),
default_branch: None,
stack_hint: None,
tags: None,
description: None,
gh_user: None,
gh_account: None,
github: Some(GithubConfig {
config_dir: Some(PathBuf::from("/home/bob/.config/gh-work")),
token_env: None,
account: Some("bob-work".into()),
host: None,
}),
commit_name: Some("Bob (work bot)".into()),
commit_email: Some("bob@acme.example.com".into()),
untracked_sync: None,
worktree: None,
}],
..Default::default()
};
let yaml = serde_yaml::to_string(&cfg).expect("serialise");
assert!(yaml.contains("github:"), "yaml: {yaml}");
assert!(yaml.contains("commit_name"), "yaml: {yaml}");
assert!(yaml.contains("commit_email"), "yaml: {yaml}");
let back: TrustyToolsConfig = serde_yaml::from_str(&yaml).expect("deserialise");
assert_eq!(cfg, back);
let minimal = TrustyToolsConfig {
projects: vec![ProjectConfig {
name: "minimal".into(),
repo_url: "https://github.com/o/minimal".into(),
default_branch: None,
stack_hint: None,
tags: None,
description: None,
gh_user: None,
gh_account: None,
github: None,
commit_name: None,
commit_email: None,
untracked_sync: None,
worktree: None,
}],
..Default::default()
};
let yaml_minimal = serde_yaml::to_string(&minimal).expect("serialise");
assert!(!yaml_minimal.contains("github:"), "yaml: {yaml_minimal}");
assert!(
!yaml_minimal.contains("commit_name"),
"yaml: {yaml_minimal}"
);
assert!(
!yaml_minimal.contains("commit_email"),
"yaml: {yaml_minimal}"
);
}
#[test]
fn daemon_config_yaml_round_trip() {
let cfg = TrustyToolsConfig {
daemon: Some(DaemonConfig {
allow_mcp_spawn: Some(true),
}),
..Default::default()
};
let yaml = serde_yaml::to_string(&cfg).expect("serialise");
let back: TrustyToolsConfig = serde_yaml::from_str(&yaml).expect("deserialise");
assert_eq!(cfg, back);
assert!(yaml.contains("allow_mcp_spawn"), "yaml: {yaml}");
let empty = TrustyToolsConfig::default();
let yaml_empty = serde_yaml::to_string(&empty).expect("serialise");
assert!(!yaml_empty.contains("daemon"), "yaml: {yaml_empty}");
}
#[test]
fn tmux_config_yaml_round_trip() {
let cfg = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(50_000),
mouse: Some(false),
alternate_screen: Some(false),
}),
..Default::default()
};
let yaml = serde_yaml::to_string(&cfg).expect("serialise");
let back: TrustyToolsConfig = serde_yaml::from_str(&yaml).expect("deserialise");
assert_eq!(cfg, back);
assert!(yaml.contains("history_limit"), "yaml: {yaml}");
assert!(yaml.contains("mouse"), "yaml: {yaml}");
assert!(yaml.contains("alternate_screen"), "yaml: {yaml}");
let empty = TrustyToolsConfig::default();
let yaml_empty = serde_yaml::to_string(&empty).expect("serialise");
assert!(!yaml_empty.contains("tmux"), "yaml: {yaml_empty}");
}
#[test]
fn tmux_options_default_when_no_config() {
let opts = resolve_tmux_options(&TrustyToolsConfig::default());
assert_eq!(opts.history_limit, DEFAULT_TMUX_HISTORY_LIMIT);
assert_eq!(opts.mouse, DEFAULT_TMUX_MOUSE);
assert_eq!(opts.alternate_screen, DEFAULT_TMUX_ALTERNATE_SCREEN);
}
#[test]
fn tmux_options_alternate_screen_defaults_on() {
const { assert!(DEFAULT_TMUX_ALTERNATE_SCREEN) };
assert!(resolve_tmux_options(&TrustyToolsConfig::default()).alternate_screen);
let partial = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(10_000),
mouse: Some(false),
alternate_screen: None,
}),
..Default::default()
};
assert!(resolve_tmux_options(&partial).alternate_screen);
}
#[test]
fn tmux_options_alternate_screen_config_override() {
let off = TrustyToolsConfig {
tmux: Some(TmuxConfig {
alternate_screen: Some(false),
..Default::default()
}),
..Default::default()
};
assert!(!resolve_tmux_options(&off).alternate_screen);
let on = TrustyToolsConfig {
tmux: Some(TmuxConfig {
alternate_screen: Some(true),
..Default::default()
}),
..Default::default()
};
assert!(resolve_tmux_options(&on).alternate_screen);
}
#[test]
fn tmux_alternate_screen_parses_from_yaml() {
let cfg: TrustyToolsConfig =
serde_yaml::from_str("tmux:\n alternate_screen: false\n").expect("deserialise");
assert_eq!(
cfg.tmux.as_ref().and_then(|t| t.alternate_screen),
Some(false)
);
assert!(!resolve_tmux_options(&cfg).alternate_screen);
}
#[test]
fn tmux_options_config_override() {
let cfg = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(250_000),
mouse: Some(false),
alternate_screen: Some(false),
}),
..Default::default()
};
let opts = resolve_tmux_options(&cfg);
assert_eq!(opts.history_limit, 250_000);
assert!(!opts.mouse);
let partial = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(10_000),
mouse: None,
alternate_screen: None,
}),
..Default::default()
};
let opts = resolve_tmux_options(&partial);
assert_eq!(opts.history_limit, 10_000);
assert_eq!(opts.mouse, DEFAULT_TMUX_MOUSE);
}
#[test]
fn tmux_options_clamps_zero_history_limit() {
let cfg = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(0),
mouse: None,
alternate_screen: None,
}),
..Default::default()
};
let opts = resolve_tmux_options(&cfg);
assert_eq!(opts.history_limit, MIN_TMUX_HISTORY_LIMIT);
}
#[test]
fn tmux_options_clamps_below_minimum() {
let cfg = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(42),
mouse: None,
alternate_screen: None,
}),
..Default::default()
};
let opts = resolve_tmux_options(&cfg);
assert_eq!(opts.history_limit, MIN_TMUX_HISTORY_LIMIT);
let at_floor = TrustyToolsConfig {
tmux: Some(TmuxConfig {
history_limit: Some(MIN_TMUX_HISTORY_LIMIT),
mouse: None,
alternate_screen: None,
}),
..Default::default()
};
assert_eq!(
resolve_tmux_options(&at_floor).history_limit,
MIN_TMUX_HISTORY_LIMIT
);
}
#[test]
fn subpath_nests_owner_repo() {
let _g = env_lock();
unsafe { std::env::remove_var(WORKSPACE_ROOT_ENV) };
let cfg = TrustyToolsConfig {
workspace_root_template: Some("/projects".into()),
..Default::default()
};
let gh = GithubPath {
owner: "bobmatnyc".into(),
repo: "trusty-tools".into(),
};
assert_eq!(
workspace_subpath(&cfg, &gh),
PathBuf::from("/projects/bobmatnyc/trusty-tools")
);
}