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.history_limit, 10_000);
assert_eq!(opts.mouse, DEFAULT_TMUX_MOUSE);
assert_eq!(opts.alternate_screen, DEFAULT_TMUX_ALTERNATE_SCREEN);
}
#[test]
fn tmux_options_alternate_screen_defaults_off() {
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(50_000),
mouse: None,
alternate_screen: None,
}),
..Default::default()
};
let opts = resolve_tmux_options(&partial);
assert_eq!(opts.history_limit, 50_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")
);
}
#[test]
fn disk_keep_list_defaults_to_empty() {
let config = TrustyToolsConfig::default();
assert!(config.disk.is_none());
assert!(disk_keep_list_patterns(&config).is_empty());
}
#[test]
fn disk_config_yaml_round_trip() {
let yaml = "disk:\n keep_list:\n - ~/work/hotstats\n - '**/scratch-*'\n";
let config: TrustyToolsConfig = serde_yaml::from_str(yaml).expect("parse the disk section");
assert_eq!(
disk_keep_list_patterns(&config),
vec!["~/work/hotstats".to_string(), "**/scratch-*".to_string()]
);
let back = serde_yaml::to_string(&config).expect("serialize");
assert!(back.contains("keep_list"), "{back}");
assert!(back.contains("hotstats"), "{back}");
}
#[test]
fn max_usage_pct_round_trips() {
let yaml = "disk:\n max_usage_pct: 85\n";
let config: TrustyToolsConfig = serde_yaml::from_str(yaml).expect("parse the disk section");
assert_eq!(
config.disk.as_ref().and_then(|d| d.max_usage_pct),
Some(85),
"the operator's threshold must reach the gate"
);
let back = serde_yaml::to_string(&config).expect("serialize");
assert!(back.contains("max_usage_pct: 85"), "{back}");
let keep_only: TrustyToolsConfig = serde_yaml::from_str("disk:\n keep_list:\n - /a\n")
.expect("parse a disk section without the key");
assert_eq!(keep_only.disk.as_ref().and_then(|d| d.max_usage_pct), None);
let back = serde_yaml::to_string(&keep_only).expect("serialize");
assert!(
!back.contains("max_usage_pct"),
"an absent threshold must not be written back: {back}"
);
}
fn config_home(yaml: &str) -> tempfile::TempDir {
let home = tempfile::tempdir().expect("tempdir");
let path = trusty_common::crate_config::crate_config_path_at(home.path(), CRATE_NAME);
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, yaml).expect("write config");
home
}
#[test]
fn an_absent_config_yields_an_empty_keep_list() {
let home = tempfile::tempdir().expect("tempdir");
let list = load_disk_keep_list_at(home.path());
assert_eq!(
list.error(),
None,
"an absent file is not an unreadable one"
);
assert!(list.patterns().is_empty());
assert!(
list.keeps(std::path::Path::new("/anything")).is_none(),
"the documented default must keep nothing"
);
}
#[test]
fn a_valid_config_yields_the_operators_patterns() {
let home = config_home("disk:\n keep_list:\n - /work/hotstats\n");
let list = load_disk_keep_list_at(home.path());
assert_eq!(list.error(), None);
assert_eq!(list.patterns(), ["/work/hotstats".to_string()]);
assert!(
list.keeps(std::path::Path::new("/work/hotstats/wt"))
.is_some()
);
}
#[test]
fn an_unparseable_config_yields_an_unreadable_keep_list() {
let home =
config_home("disk:\n keep_list:\n - /work/hotstats\nauto_resume: not-a-boolean\n");
let lenient = trusty_common::crate_config::load_at::<TrustyToolsConfig>(
&trusty_common::crate_config::crate_config_path_at(home.path(), CRATE_NAME),
);
assert!(lenient.is_err(), "the fixture must actually be malformed");
let list = load_disk_keep_list_at(home.path());
let error = list
.error()
.expect("an unparseable config must be reported, not silently defaulted");
assert!(
error.contains("YAML"),
"the reason must name the parse failure: {error}"
);
assert!(
list.keeps(std::path::Path::new("/anything/at/all"))
.is_some(),
"a keep-list that could not be read must keep EVERYTHING"
);
}