use super::super::test_helpers::FakeTmux;
use super::*;
const TEST_SESSION_ID: &str = "11111111-2222-3333-4444-555555555555";
const TEST_CWD: &str = "/tmp/ws";
struct HomeGuard {
prev: Option<String>,
_tmp: tempfile::TempDir,
}
impl HomeGuard {
fn set() -> Self {
let tmp = tempfile::tempdir().expect("tempdir");
let prev = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
Self { prev, _tmp: tmp }
}
}
impl Drop for HomeGuard {
fn drop(&mut self) {
match self.prev {
Some(ref p) => unsafe { std::env::set_var("HOME", p) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
#[test]
fn claude_code_adapter_identifies() {
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake);
assert_eq!(adapter.identify(), "claude-code");
}
#[test]
fn spawn_command_contains_env_scrub() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY"),
"spawn command must contain env scrub: {cmd}"
);
assert!(
cmd.contains(" claude "),
"spawn command must invoke claude: {cmd}"
);
}
#[test]
fn spawn_command_exports_managed_session_id() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
let expected_prefix = format!("export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; ");
assert!(
cmd.contains(&format!("{{ {expected_prefix}")),
"spawn command must export the session id as the first statement \
inside the cd-group: {cmd}"
);
let export_pos = cmd
.find("export TM_MANAGED_SESSION_ID")
.expect("export present");
let claude_pos = cmd.find(" claude ").expect("claude invocation present");
assert!(
export_pos < claude_pos,
"export must precede the claude invocation: {cmd}"
);
}
#[test]
fn spawn_command_sets_claude_config_dir() {
let dir = Path::new("/home/bob/.trusty-tools/trusty-mpm/claude-config");
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY -u "),
"the API-key scrub must still lead the env prefix: {cmd}"
);
assert!(
cmd.contains("CLAUDE_CONFIG_DIR='/home/bob/.trusty-tools/trusty-mpm/claude-config' claude"),
"spawn command must scrub the key (via -u, BEFORE the NAME=VALUE assignment per \
POSIX env grammar) then set (single-quoted) CLAUDE_CONFIG_DIR: {cmd}"
);
assert!(
cmd.contains("--setting-sources user,project,local"),
"a relocated spawn must load the `user` tier: {cmd}"
);
}
#[test]
fn spawn_command_loads_the_user_tier_only_when_config_dir_is_relocated() {
let relocated = spawn_command(
Path::new(TEST_CWD),
"claude",
Some(Path::new(
"/home/bob/.trusty-tools/trusty-mpm/claude-config",
)),
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
relocated.contains("--setting-sources user,project,local"),
"relocated spawn must load the tm-owned `user` tier: {relocated}"
);
let ambient = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
ambient.contains("--setting-sources project,local"),
"non-relocated spawn must keep the #1269 exclusion: {ambient}"
);
assert!(
!ambient.contains("user,project,local"),
"without CLAUDE_CONFIG_DIR the `user` tier is the operator's real \
~/.claude and must stay excluded (#1269): {ambient}"
);
}
#[test]
fn resume_command_loads_the_user_tier_when_config_dir_is_relocated() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
Some(Path::new(
"/home/bob/.trusty-tools/trusty-mpm/claude-config",
)),
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("--setting-sources user,project,local"),
"relocated resume must load the tm-owned `user` tier: {cmd}"
);
}
#[test]
fn env_bin_prefix_orders_unset_flag_before_config_dir_assignment() {
let dir = Path::new("/home/bob/.trusty-tools/trusty-mpm/claude-config");
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
TEST_SESSION_ID,
None,
None,
None,
);
let u_pos = cmd
.find("-u ANTHROPIC_API_KEY")
.expect("cmd must contain -u ANTHROPIC_API_KEY");
let config_pos = cmd
.find("CLAUDE_CONFIG_DIR=")
.expect("cmd must contain CLAUDE_CONFIG_DIR=");
assert!(
u_pos < config_pos,
"-u ANTHROPIC_API_KEY must appear BEFORE CLAUDE_CONFIG_DIR= per POSIX env \
option-then-assignment grammar: {cmd}"
);
}
#[test]
fn spawn_command_without_config_dir_omits_it() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("CLAUDE_CONFIG_DIR"),
"no config dir → must not reference CLAUDE_CONFIG_DIR: {cmd}"
);
}
#[test]
fn spawn_command_sets_oauth_token_when_available() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
Some("sk-ant-oat01-fake-token"),
None,
);
assert!(
cmd.contains("CLAUDE_CODE_OAUTH_TOKEN='sk-ant-oat01-fake-token'"),
"spawn command must inject a single-quoted CLAUDE_CODE_OAUTH_TOKEN when \
one is available: {cmd}"
);
assert!(
cmd.contains("-u ANTHROPIC_API_KEY"),
"the API-key scrub must still be present alongside the token: {cmd}"
);
}
#[test]
fn spawn_command_omits_oauth_token_when_absent() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("CLAUDE_CODE_OAUTH_TOKEN"),
"no token available → must not reference CLAUDE_CODE_OAUTH_TOKEN: {cmd}"
);
}
#[test]
fn spawn_command_without_token_pins_the_exact_command() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
let scrub = crate::core::claude_env_scrub::env_unset_flags();
let expected = format!(
"cd '/tmp/ws' && {{ export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; \
env -u ANTHROPIC_API_KEY{scrub} claude \
--setting-sources project,local --dangerously-skip-permissions; \
echo 'tm: run `tm` to relaunch this session'; }}"
);
assert_eq!(cmd, expected, "no-token command shape must stay pinned");
}
#[test]
fn spawn_command_scrubs_inherited_session_markers() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("-u CLAUDE_CODE_CHILD_SESSION"),
"spawn must unset the marker that disables transcript saving: {cmd}"
);
assert!(
cmd.contains("-u CLAUDE_CODE_SESSION_ID"),
"spawn must unset the parent's session id: {cmd}"
);
assert!(
cmd.contains("-u CLAUDECODE"),
"spawn must unset the inherited in-Claude-Code marker: {cmd}"
);
}
#[test]
fn resume_command_scrubs_inherited_session_markers() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("-u CLAUDE_CODE_CHILD_SESSION"),
"resume must unset the marker that disables transcript saving: {cmd}"
);
assert!(
cmd.contains("--resume abc-123"),
"the resume selection must survive the scrub: {cmd}"
);
}
#[test]
fn spawn_command_keeps_config_dir_out_of_the_scrub() {
let dir = Path::new("/home/bob/.trusty-tools/trusty-mpm/claude-config");
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("-u CLAUDE_CONFIG_DIR"),
"CLAUDE_CONFIG_DIR must never be unset (#4455 depends on it): {cmd}"
);
assert!(
cmd.contains("CLAUDE_CONFIG_DIR='/home/bob/.trusty-tools/trusty-mpm/claude-config' claude"),
"CLAUDE_CONFIG_DIR must still be assigned right before the binary: {cmd}"
);
}
#[test]
fn env_bin_prefix_orders_scrub_flags_before_assignments() {
let dir = Path::new("/tm/config");
let prefix = env_bin_prefix("/abs/claude", Some(dir), Some("tok"));
let last_unset = prefix
.rfind("-u ")
.expect("prefix must contain at least one -u flag");
let first_assignment = prefix
.find("CLAUDE_CONFIG_DIR=")
.expect("prefix must contain the config-dir assignment");
assert!(
last_unset < first_assignment,
"every -u flag must precede the first NAME=VALUE assignment: {prefix}"
);
let unset = crate::core::claude_env_scrub::parse_env_unset_vars(&prefix);
assert!(
unset.contains(&"CLAUDE_CODE_CHILD_SESSION"),
"the real spawn prefix must unset the suppressing marker: {unset:?}"
);
assert!(
!unset.contains(&"CLAUDE_CONFIG_DIR"),
"the real spawn prefix must NOT unset CLAUDE_CONFIG_DIR: {unset:?}"
);
}
#[test]
fn spawn_command_sets_both_config_dir_and_oauth_token() {
let dir = Path::new("/home/bob/.trusty-tools/trusty-mpm/claude-config");
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
TEST_SESSION_ID,
None,
Some("sk-ant-oat01-fake-token"),
None,
);
assert!(
cmd.contains(
"CLAUDE_CONFIG_DIR='/home/bob/.trusty-tools/trusty-mpm/claude-config' \
CLAUDE_CODE_OAUTH_TOKEN='sk-ant-oat01-fake-token' claude"
),
"both assignments must coexist, config dir before the token: {cmd}"
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY -u "),
"the API-key scrub must still lead the env prefix: {cmd}"
);
}
#[test]
fn resume_command_sets_oauth_token_when_available() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
Some("sk-ant-oat01-fake-token"),
None,
);
assert!(
cmd.contains("CLAUDE_CODE_OAUTH_TOKEN='sk-ant-oat01-fake-token'"),
"resume command must inject the token when available: {cmd}"
);
assert!(
cmd.contains("--resume abc-123"),
"--resume must still be present alongside the token: {cmd}"
);
}
#[test]
fn resume_command_omits_oauth_token_when_absent() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("CLAUDE_CODE_OAUTH_TOKEN"),
"no token available → must not reference CLAUDE_CODE_OAUTH_TOKEN: {cmd}"
);
}
#[test]
fn resume_command_without_token_pins_the_exact_command() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
let scrub = crate::core::claude_env_scrub::env_unset_flags();
let expected = format!(
"cd '/tmp/ws' && {{ export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; \
env -u ANTHROPIC_API_KEY{scrub} claude \
--setting-sources project,local --dangerously-skip-permissions --resume abc-123; \
echo 'tm: run `tm` to relaunch this session'; }}"
);
assert_eq!(
cmd, expected,
"no-token resume command shape must stay pinned"
);
}
#[test]
fn env_bin_prefix_quotes_config_dir_with_space() {
let dir = Path::new("/Users/John Doe/.trusty-tools/trusty-mpm/claude-config");
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains(
"CLAUDE_CONFIG_DIR='/Users/John Doe/.trusty-tools/trusty-mpm/claude-config' claude"
),
"config dir with a space must be single-quoted and intact: {cmd}"
);
assert!(
!cmd.contains("CLAUDE_CONFIG_DIR=/Users/John Doe"),
"config dir must never be interpolated unquoted: {cmd}"
);
let resume = resume_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
None,
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
resume
.contains("CLAUDE_CONFIG_DIR='/Users/John Doe/.trusty-tools/trusty-mpm/claude-config'"),
"resume command must also single-quote a spaced config dir: {resume}"
);
}
#[test]
fn shell_single_quote_escapes_embedded_quote() {
assert_eq!(shell_single_quote("/a/b"), "'/a/b'");
assert_eq!(
shell_single_quote("/Users/o'brien/cfg"),
r"'/Users/o'\''brien/cfg'"
);
}
#[test]
fn spawn_command_contains_isolation_flags() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("--setting-sources project,local"),
"spawn command must isolate settings: {cmd}"
);
assert!(
cmd.contains("--dangerously-skip-permissions"),
"spawn command must bypass permissions for unattended orchestration: {cmd}"
);
assert!(
!cmd.contains("acceptEdits"),
"old acceptEdits flag must not be present: {cmd}"
);
}
#[test]
fn spawn_command_uses_resolved_binary() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"/Users/me/.local/bin/claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains(" /Users/me/.local/bin/claude "),
"spawn command must invoke the resolved absolute claude path: {cmd}"
);
}
#[test]
fn claude_code_adapter_binary_check_returns_option() {
if let Some(p) = ClaudeCodeAdapter::resolve_claude() {
assert!(!p.is_empty(), "resolved claude path must be non-empty");
}
}
#[serial_test::serial]
#[test]
fn spawn_sends_env_scrub_when_binary_available() {
let _home = HomeGuard::set();
let Some(_claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
return;
};
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn(
"tmpm-test",
Path::new("/tmp"),
"some task",
TEST_SESSION_ID,
&[],
)
.expect("spawn");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert_eq!(sends[0].0, "tmpm-test");
let cmd = &sends[0].1;
assert!(cmd.contains("-u ANTHROPIC_API_KEY"));
assert!(
cmd.contains("--append-system-prompt-file"),
"spawn command must inject the PM system prompt file: {cmd}"
);
}
#[serial_test::serial]
#[test]
fn spawn_sends_oauth_token_when_available() {
let _home = HomeGuard::set();
let Some(_claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
return;
};
unsafe {
std::env::set_var(
crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR,
"sk-ant-oat01-fake-token",
);
}
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
let result = adapter.spawn(
"tmpm-test",
Path::new("/tmp"),
"some task",
TEST_SESSION_ID,
&[],
);
unsafe {
std::env::remove_var(crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR);
}
result.expect("spawn");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert!(
sends[0]
.1
.contains("CLAUDE_CODE_OAUTH_TOKEN='sk-ant-oat01-fake-token'"),
"spawn must inject the resolved oauth token: {}",
sends[0].1
);
}
#[test]
fn publish_session_env_sets_id_and_config_dir() {
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter.publish_session_env("tmpm-test", TEST_SESSION_ID, Some("/tmp/config-dir"));
let env_sets = fake.env_sets.lock().unwrap();
assert_eq!(
env_sets.len(),
2,
"expected id + config-dir sets: {env_sets:?}"
);
assert!(env_sets.contains(&(
"tmpm-test".to_string(),
"TM_MANAGED_SESSION_ID".to_string(),
TEST_SESSION_ID.to_string()
)));
assert!(env_sets.contains(&(
"tmpm-test".to_string(),
"CLAUDE_CONFIG_DIR".to_string(),
"/tmp/config-dir".to_string()
)));
}
#[test]
fn publish_session_env_omits_config_dir_when_absent() {
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter.publish_session_env("tmpm-test", TEST_SESSION_ID, None);
let env_sets = fake.env_sets.lock().unwrap();
assert_eq!(
env_sets.len(),
1,
"expected only the session-id set: {env_sets:?}"
);
assert_eq!(env_sets[0].1, "TM_MANAGED_SESSION_ID");
}
#[serial_test::serial]
#[test]
fn spawn_publishes_session_id_via_set_environment() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
}
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn(
"tmpm-test",
Path::new("/tmp"),
"some task",
TEST_SESSION_ID,
&[],
)
.expect("spawn");
let env_sets = fake.env_sets.lock().unwrap();
assert!(
env_sets.iter().any(|(name, key, value)| name == "tmpm-test"
&& key == "TM_MANAGED_SESSION_ID"
&& value == TEST_SESSION_ID),
"spawn must call tmux set-environment with TM_MANAGED_SESSION_ID: {env_sets:?}"
);
}
#[test]
fn spawn_command_with_prompt_file_contains_flag() {
let path = Path::new("/tmp/trusty-mpm-system-prompt-test.txt");
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
Some(path),
None,
None,
);
assert!(
cmd.contains("--append-system-prompt-file '/tmp/trusty-mpm-system-prompt-test.txt'"),
"spawn command must pass the prompt file via --append-system-prompt-file: {cmd}"
);
assert!(
cmd.contains("--setting-sources project,local"),
"isolation flags must still be present alongside the prompt file: {cmd}"
);
}
#[test]
fn spawn_command_without_prompt_file_omits_flag() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("--append-system-prompt-file"),
"no prompt file → flag must be absent: {cmd}"
);
}
#[test]
#[serial_test::serial]
fn build_prompt_file_writes_resolved_prompt_for_project() {
let _home = HomeGuard::set();
let tmp = tempfile::tempdir().expect("tempdir");
let path = build_prompt_file(tmp.path(), Some("sess-1")).expect("prompt file written");
let content = std::fs::read_to_string(&path).expect("prompt file readable");
assert!(
content.contains("# PM Agent -- Trusty MPM"),
"prompt file must contain the resolved PM system prompt: {content}"
);
std::fs::remove_file(&path).ok();
}
#[test]
#[serial_test::serial]
fn build_prompt_file_refreshes_the_compiled_prompt() {
let _home = HomeGuard::set();
let tmp = tempfile::tempdir().expect("tempdir");
let compiled = crate::core::instruction_pipeline::compiled_prompt_path(tmp.path(), "sess-1");
std::fs::create_dir_all(compiled.parent().unwrap()).expect("create framework dir");
const STALE: &str = "STALE-FROM-A-PREVIOUS-LAUNCH";
std::fs::write(&compiled, STALE).expect("seed stale compiled prompt");
let path = build_prompt_file(tmp.path(), Some("sess-1")).expect("prompt file written");
let on_disk = std::fs::read_to_string(&compiled).expect("compiled prompt readable");
assert_ne!(
on_disk, STALE,
"the spawn seam must refresh a stale compiled prompt — resume paths \
depend on this, since they never run prepare_session"
);
let launch_prompt = std::fs::read_to_string(&path).expect("prompt file readable");
assert_eq!(
on_disk, launch_prompt,
"the compiled prompt must be byte-identical to the file passed to \
--append-system-prompt-file"
);
std::fs::remove_file(&path).ok();
}
#[test]
#[serial_test::serial]
fn build_prompt_file_compiled_write_failure_does_not_block_the_spawn() {
let _home = HomeGuard::set();
let tmp = tempfile::tempdir().expect("tempdir");
let compiled = crate::core::instruction_pipeline::compiled_prompt_path(tmp.path(), "sess-1");
std::fs::create_dir_all(&compiled).expect("plant a directory at the compiled path");
let path = build_prompt_file(tmp.path(), Some("sess-1"))
.expect("spawn must still get its prompt file (non-fatal)");
let content = std::fs::read_to_string(&path).expect("prompt file readable");
assert!(
content.contains("# PM Agent -- Trusty MPM"),
"the session must still receive the real PM prompt: {content}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn resume_command_with_id_uses_resume_flag() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("--resume abc-123"),
"resume command must include --resume <id>: {cmd}"
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY"),
"resume command must still scrub API key: {cmd}"
);
assert!(
!cmd.contains("--continue"),
"resume command must NOT contain --continue when id is set: {cmd}"
);
}
#[test]
fn resume_command_exports_managed_session_id() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
let expected_prefix = format!("export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; ");
assert!(
cmd.contains(&format!("{{ {expected_prefix}")),
"resume command must export the session id as the first statement \
inside the cd-group: {cmd}"
);
let export_pos = cmd
.find("export TM_MANAGED_SESSION_ID")
.expect("export present");
let resume_pos = cmd.find("--resume").expect("--resume flag present");
assert!(
export_pos < resume_pos,
"export must precede the --resume flag: {cmd}"
);
}
#[test]
fn resume_command_sets_claude_config_dir() {
let dir = Path::new("/home/bob/.trusty-tools/trusty-mpm/claude-config");
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
Some(dir),
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY -u "),
"the API-key scrub must still lead the env prefix: {cmd}"
);
assert!(
cmd.contains("CLAUDE_CONFIG_DIR='/home/bob/.trusty-tools/trusty-mpm/claude-config' claude"),
"resume command must export (single-quoted) CLAUDE_CONFIG_DIR after the -u option: {cmd}"
);
assert!(
cmd.contains("--resume abc-123"),
"resume command must still include --resume <id>: {cmd}"
);
}
#[test]
fn resume_command_without_id_with_prior_conv_uses_continue() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
None,
true,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("--continue"),
"resume command without id + prior conv must use --continue: {cmd}"
);
assert!(
!cmd.contains("--resume"),
"resume command without id must NOT contain --resume: {cmd}"
);
}
#[test]
fn resume_command_without_id_no_prior_conv_uses_plain_spawn() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
None,
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("--continue"),
"resume command without id + no prior conv must NOT use --continue: {cmd}"
);
assert!(
!cmd.contains("--resume"),
"resume command without id must NOT use --resume: {cmd}"
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY"),
"plain spawn command must still scrub API key: {cmd}"
);
}
#[test]
fn has_prior_conversation_returns_false_for_fresh_workspace() {
let tmp = tempfile::tempdir().expect("tempdir");
let projects_dir = tmp.path().join("projects");
assert!(
!has_prior_conversation_in(tmp.path(), &projects_dir),
"no projects dir → false"
);
std::fs::create_dir_all(&projects_dir).unwrap();
assert!(
!has_prior_conversation_in(tmp.path(), &projects_dir),
"projects dir exists but no entry for this workspace → false"
);
}
#[test]
fn has_prior_conversation_returns_true_when_jsonl_exists() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("my-workspace");
std::fs::create_dir_all(&cwd).unwrap();
let projects_dir = tmp.path().join("projects");
let encoded = cwd.to_string_lossy().replace('/', "-");
let project_dir = projects_dir.join(&encoded);
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("session.jsonl"), "{}").unwrap();
assert!(
has_prior_conversation_in(&cwd, &projects_dir),
"workspace with .jsonl must return true"
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_with_id_uses_resume_flag() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
};
let cwd = Path::new("/tmp");
let config_dir = crate::core::trusty_tools_config::managed_claude_config_dir()
.expect("config dir resolves under redirected HOME");
let encoded = cwd.to_string_lossy().replace('/', "-");
let project_dir = config_dir.join("projects").join(&encoded);
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("my-session-id.jsonl"), "{}").unwrap();
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"tmpm-test",
None,
cwd,
"task",
Some("my-session-id"),
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert!(
sends[0].1.contains("--resume my-session-id"),
"spawn_resume with an existing id must use --resume: {}",
sends[0].1
);
let env_sets = fake.env_sets.lock().unwrap();
assert!(
env_sets.iter().any(|(name, key, value)| name == "tmpm-test"
&& key == "TM_MANAGED_SESSION_ID"
&& value == TEST_SESSION_ID),
"spawn_resume must call tmux set-environment with TM_MANAGED_SESSION_ID: {env_sets:?}"
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_sends_prompt_file_when_binary_available() {
let _home = HomeGuard::set();
let Some(_claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
return;
};
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"tmpm-test",
None,
Path::new("/tmp/does-not-exist-2230"),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert!(
sends[0].1.contains("--append-system-prompt-file"),
"spawn_resume must inject the PM system prompt file: {}",
sends[0].1
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_sends_oauth_token_when_available() {
let _home = HomeGuard::set();
let Some(_claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
return;
};
unsafe {
std::env::set_var(
crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR,
"sk-ant-oat01-fake-token",
);
}
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
let result = adapter.spawn_resume(
"tmpm-test",
None,
Path::new("/tmp/does-not-exist-2246"),
"task",
None,
TEST_SESSION_ID,
&[],
);
unsafe {
std::env::remove_var(crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR);
}
result.expect("spawn_resume");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert!(
sends[0]
.1
.contains("CLAUDE_CODE_OAUTH_TOKEN='sk-ant-oat01-fake-token'"),
"spawn_resume must inject the resolved oauth token: {}",
sends[0].1
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_with_missing_id_falls_back_gracefully() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
};
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"tmpm-test",
None,
Path::new("/tmp/does-not-exist-2013"),
"task",
Some("stale-session-id"),
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume must not hard-fail on a missing id");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert!(
!sends[0].1.contains("--resume"),
"a missing id must NOT be passed to --resume: {}",
sends[0].1
);
assert!(
!sends[0].1.contains("--continue"),
"a missing id with no prior conversation history must fall back to a \
plain spawn, not --continue: {}",
sends[0].1
);
assert!(
sends[0].1.contains("env -u ANTHROPIC_API_KEY"),
"fallback command must still scrub the API key: {}",
sends[0].1
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_targets_stored_pane_id_when_known() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
};
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"tmpm-test",
Some("%6015"),
Path::new("/tmp/does-not-exist-pane-target"),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume with known pane_id");
let pane_sends = fake.pane_sends.lock().unwrap();
assert_eq!(
pane_sends.len(),
1,
"spawn_resume with a known pane_id must call send_line_to_pane exactly once: \
{pane_sends:?}"
);
assert_eq!(
pane_sends[0].0, "tmpm-test",
"pane-scoped send must target the record's tmux session name"
);
assert_eq!(
pane_sends[0].1, "%6015",
"pane-scoped send must target the record's STORED pane_id, not whatever pane \
tmux considers active"
);
let sends = fake.sends.lock().unwrap();
assert!(
sends.is_empty(),
"spawn_resume with a known pane_id must NEVER fall back to the session-scoped \
send_line (which tmux resolves to the active pane): {sends:?}"
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_falls_back_to_session_target_when_pane_id_unknown() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
};
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"tmpm-test",
None,
Path::new("/tmp/does-not-exist-no-pane"),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume with no pane_id");
let sends = fake.sends.lock().unwrap();
assert_eq!(
sends.len(),
1,
"spawn_resume with no pane_id must fall back to session-scoped send_line: {sends:?}"
);
let pane_sends = fake.pane_sends.lock().unwrap();
assert!(
pane_sends.is_empty(),
"spawn_resume with no pane_id must never call send_line_to_pane: {pane_sends:?}"
);
}
#[test]
fn session_id_exists_true_for_real_jsonl_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("my-workspace");
std::fs::create_dir_all(&cwd).unwrap();
let projects_dir = tmp.path().join("projects");
let encoded = cwd.to_string_lossy().replace('/', "-");
let project_dir = projects_dir.join(&encoded);
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("abc-123.jsonl"), "{}").unwrap();
assert!(
session_id_exists_in(&cwd, &projects_dir, "abc-123"),
"existing session file must resolve as present"
);
}
#[test]
fn session_id_exists_false_for_missing_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("my-workspace");
std::fs::create_dir_all(&cwd).unwrap();
let projects_dir = tmp.path().join("projects");
let encoded = cwd.to_string_lossy().replace('/', "-");
let project_dir = projects_dir.join(&encoded);
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("other-session.jsonl"), "{}").unwrap();
assert!(
!session_id_exists_in(&cwd, &projects_dir, "stale-id-not-present"),
"id with no matching file must resolve as absent"
);
}
#[test]
fn session_id_exists_false_when_projects_dir_absent() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing_projects_dir = tmp.path().join("does-not-exist");
assert!(
!session_id_exists_in(tmp.path(), &missing_projects_dir, "any-id"),
"missing projects dir must resolve as absent, not panic"
);
assert!(
!session_id_exists(tmp.path(), None, "any-id"),
"session_id_exists with no config dir falls back to home resolution \
and must never panic even if HOME is unusual in the test env"
);
}
#[test]
fn encode_project_dir_replaces_slashes() {
assert_eq!(
encode_project_dir(Path::new("/private/tmp/foo")),
"-private-tmp-foo"
);
}
#[test]
fn session_id_exists_finds_hardcoded_dir_name_for_known_cwd() {
let tmp = tempfile::tempdir().expect("tempdir");
let projects_dir = tmp.path().join("projects");
let project_dir = projects_dir.join("-tmp-my-workspace");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("known-id.jsonl"), "{}").unwrap();
assert!(
session_id_exists_in(Path::new("/tmp/my-workspace"), &projects_dir, "known-id"),
"session_id_exists_in must find the seeded session under the \
hand-typed expected dir name '-tmp-my-workspace'"
);
}
#[test]
fn projects_dir_for_prefers_config_dir_when_present() {
let config_dir = Path::new("/tmp/some-managed-config-dir");
assert_eq!(
projects_dir_for(Some(config_dir)),
Some(config_dir.join("projects")),
"projects_dir_for must nest under the given config dir"
);
}
#[serial_test::serial]
#[test]
fn spawn_resume_without_id_no_prior_conv_sends_plain_spawn() {
let cmd = resume_command(
Path::new(TEST_CWD),
"__fake_claude__",
None,
None,
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
!cmd.contains("--continue"),
"plain-spawn path must NOT use --continue: {cmd}"
);
assert!(
!cmd.contains("--resume"),
"plain-spawn path must NOT use --resume: {cmd}"
);
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY"),
"plain-spawn path must still scrub API key: {cmd}"
);
let _home = HomeGuard::set();
let Some(_claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
return; };
let tmp = tempfile::tempdir().expect("tempdir");
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"test-tmux-session",
None,
tmp.path(),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume without id");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert!(
!sends[0].1.contains("--continue"),
"adapter plain-spawn must NOT use --continue: {}",
sends[0].1
);
assert!(
!sends[0].1.contains("--resume"),
"adapter plain-spawn must NOT use --resume: {}",
sends[0].1
);
}
#[test]
fn spawn_command_prefixes_cd_to_workdir() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.starts_with("cd '/tmp/ws' && { "),
"spawn command must start with an explicit cd into the workdir: {cmd}"
);
assert!(
cmd.trim_end().ends_with('}'),
"spawn command must close the brace group it opened: {cmd}"
);
}
#[test]
fn resume_command_prefixes_cd_to_workdir() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.starts_with("cd '/tmp/ws' && { "),
"resume command must start with an explicit cd into the workdir: {cmd}"
);
assert!(
cmd.trim_end().ends_with('}'),
"resume command must close the brace group it opened: {cmd}"
);
}
#[test]
fn cd_and_group_quotes_workdir_with_space() {
let cmd = cd_and_group(Path::new("/Users/John Doe/work"), "echo hi");
assert_eq!(cmd, "cd '/Users/John Doe/work' && { echo hi; }");
}
#[test]
fn spawn_command_prints_relaunch_hint_after_claude_exits() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("; echo 'tm: run `tm` to relaunch this session'"),
"spawn command must print the relaunch hint after claude exits: {cmd}"
);
let claude_pos = cmd.find(" claude ").expect("claude invocation present");
let hint_pos = cmd.find("; echo").expect("relaunch hint present");
assert!(
claude_pos < hint_pos,
"relaunch hint must come AFTER the claude invocation: {cmd}"
);
}
#[test]
fn resume_command_prints_relaunch_hint_after_claude_exits() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
assert!(
cmd.contains("; echo 'tm: run `tm` to relaunch this session'"),
"resume command must print the relaunch hint after claude exits: {cmd}"
);
let resume_pos = cmd.find("--resume").expect("--resume flag present");
let hint_pos = cmd.find("; echo").expect("relaunch hint present");
assert!(
resume_pos < hint_pos,
"relaunch hint must come AFTER --resume: {cmd}"
);
}
#[test]
fn resume_command_with_prompt_file_contains_flag() {
let path = Path::new("/tmp/trusty-mpm-system-prompt-resume-test.txt");
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
Some(path),
None,
None,
);
assert!(
cmd.contains("--append-system-prompt-file '/tmp/trusty-mpm-system-prompt-resume-test.txt'"),
"resume command must pass the prompt file via --append-system-prompt-file: {cmd}"
);
assert!(
cmd.contains("--resume abc-123"),
"--resume must still be present alongside the prompt file: {cmd}"
);
}
#[test]
fn compose_inplace_args_uses_resume_for_existing_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).unwrap();
let config_dir = tmp.path().join("config");
let encoded = cwd.to_string_lossy().replace('/', "-");
let project_dir = config_dir.join("projects").join(&encoded);
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("existing-id.jsonl"), "{}").unwrap();
let args = compose_inplace_args(&cwd, Some(&config_dir), Some("existing-id"), None);
assert!(
args.windows(2).any(|w| w == ["--resume", "existing-id"]),
"must select --resume <id> for an id that exists on disk: {args:?}"
);
assert!(
!args.contains(&"--continue".to_owned()),
"must not ALSO pass --continue when --resume is used: {args:?}"
);
}
#[test]
fn compose_inplace_args_falls_back_for_missing_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("workspace-missing");
std::fs::create_dir_all(&cwd).unwrap();
let config_dir = tmp.path().join("config");
let args = compose_inplace_args(&cwd, Some(&config_dir), Some("stale-id"), None);
assert!(
!args.contains(&"--resume".to_owned()),
"a stale id must not be passed to --resume: {args:?}"
);
assert!(
!args.contains(&"--continue".to_owned()),
"no prior conversation history: must not fall back to --continue: {args:?}"
);
assert!(
args.contains(&"--setting-sources".to_owned()),
"isolation flags must still be present: {args:?}"
);
}
#[serial_test::serial]
#[test]
fn compose_inplace_args_uses_continue_when_no_id_but_prior_conv() {
let _home = HomeGuard::set();
let home = dirs::home_dir().expect("home resolves under redirected HOME");
let cwd = std::path::PathBuf::from("/tmp/inplace-continue-test");
let encoded = cwd.to_string_lossy().replace('/', "-");
let project_dir = home.join(".claude").join("projects").join(&encoded);
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("some-other-session.jsonl"), "{}").unwrap();
let args = compose_inplace_args(&cwd, None, Some("stale-id"), None);
assert!(
!args.contains(&"--resume".to_owned()),
"a stale id must not be passed to --resume: {args:?}"
);
assert!(
args.contains(&"--continue".to_owned()),
"prior conversation history exists: must fall back to --continue: {args:?}"
);
}
#[test]
fn compose_inplace_args_carries_prompt_file_unquoted() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("workspace-prompt");
std::fs::create_dir_all(&cwd).unwrap();
let prompt = tmp.path().join("system prompt.txt");
std::fs::write(&prompt, "PM").unwrap();
let args = compose_inplace_args(&cwd, None, None, Some(&prompt));
let idx = args
.iter()
.position(|a| a == "--append-system-prompt-file")
.expect("prompt flag must be present");
assert_eq!(
args[idx + 1],
prompt.display().to_string(),
"the path must be a single, UNQUOTED argv token: {args:?}"
);
assert!(
!args[idx + 1].contains('\''),
"shell quoting must not leak into an execv argv token: {args:?}"
);
assert!(
args.contains(&"--dangerously-skip-permissions".to_owned()),
"the isolation flags must still follow the prompt file: {args:?}"
);
}
#[test]
fn compose_inplace_args_omits_prompt_flag_when_absent() {
let tmp = tempfile::tempdir().expect("tempdir");
let args = compose_inplace_args(tmp.path(), None, None, None);
assert!(
!args.contains(&"--append-system-prompt-file".to_owned()),
"no prompt file → no flag: {args:?}"
);
assert!(
args.contains(&"--setting-sources".to_owned()),
"isolation flags are unconditional: {args:?}"
);
}
#[test]
fn compose_inplace_args_loads_the_user_tier_when_config_dir_is_relocated() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("workspace-inplace-tier");
std::fs::create_dir_all(&cwd).unwrap();
let config_dir = tmp.path().join("claude-config");
let relocated = compose_inplace_args(&cwd, Some(&config_dir), None, None);
let idx = relocated
.iter()
.position(|a| a == "--setting-sources")
.expect("setting-sources flag must be present");
assert_eq!(
relocated[idx + 1],
"user,project,local",
"relocated in-place relaunch must load the tm-owned `user` tier: {relocated:?}"
);
let ambient = compose_inplace_args(&cwd, None, None, None);
let idx = ambient
.iter()
.position(|a| a == "--setting-sources")
.expect("setting-sources flag must be present");
assert_eq!(
ambient[idx + 1],
"project,local",
"without a relocated config dir the #1269 exclusion stands: {ambient:?}"
);
}
#[serial_test::serial]
#[test]
fn build_inplace_resume_command_carries_prompt_file() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let result = build_inplace_resume_command(tmp.path(), None).expect("build succeeds");
let idx = result
.args
.iter()
.position(|a| a == "--append-system-prompt-file")
.expect("in-place relaunch must carry the PM system prompt");
assert!(
std::path::Path::new(&result.args[idx + 1]).is_file(),
"the prompt token must name a written file: {:?}",
result.args
);
}
#[serial_test::serial]
#[test]
fn build_inplace_resume_command_resolves_claude_binary() {
let _home = HomeGuard::set();
let Some(claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
return;
};
let tmp = tempfile::tempdir().expect("tempdir");
let result = build_inplace_resume_command(tmp.path(), Some("some-id")).expect("build succeeds");
assert_eq!(result.claude_bin, claude_bin);
assert!(
result.args.contains(&"--setting-sources".to_owned()),
"isolation flags must be present: {:?}",
result.args
);
}
#[serial_test::serial]
#[test]
fn build_inplace_resume_command_carries_oauth_token_when_available() {
let _home = HomeGuard::set();
if ClaudeCodeAdapter::resolve_claude().is_none() {
return;
}
unsafe {
std::env::set_var(
crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR,
"sk-ant-oat01-fake-token",
);
}
let tmp = tempfile::tempdir().expect("tempdir");
let result = build_inplace_resume_command(tmp.path(), None);
unsafe {
std::env::remove_var(crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR);
}
let result = result.expect("build succeeds");
assert_eq!(
result.oauth_token.as_deref(),
Some("sk-ant-oat01-fake-token")
);
}
#[serial_test::serial]
#[test]
fn prepare_managed_config_pins_all_builtins_on_success() {
let _home = HomeGuard::set();
let cwd_root = tempfile::tempdir().expect("tempdir");
let cwd = cwd_root.path();
let config_dir = prepare_managed_config("test-session", cwd)
.expect("prepare_managed_config must resolve a config dir under the redirected HOME");
let mcp: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(cwd.join(".mcp.json"))
.expect("prepare_managed_config must write cwd/.mcp.json"),
)
.expect("cwd/.mcp.json must be valid JSON");
for name in [
"trusty-mpm",
"trusty-review",
"trusty-memory",
"trusty-search",
] {
assert_eq!(
mcp["mcpServers"][name]["command"],
serde_json::json!(name),
"{name} must be pinned to its canonical command on a clean run: {mcp}"
);
}
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(config_dir.join(".claude.json"))
.expect("prepare_managed_config must write <config_dir>/.claude.json"),
)
.expect("<config_dir>/.claude.json must be valid JSON");
let key = cwd.to_string_lossy().to_string();
let enabled: Vec<&str> = value["projects"][&key]["enabledMcpjsonServers"]
.as_array()
.expect("enabledMcpjsonServers is an array")
.iter()
.filter_map(|v| v.as_str())
.collect();
for name in [
"trusty-mpm",
"trusty-review",
"trusty-memory",
"trusty-search",
] {
assert!(
enabled.contains(&name),
"{name} must be approved when its pin succeeds on a clean run: {enabled:?}"
);
}
}
#[serial_test::serial]
#[test]
fn prepare_managed_config_excludes_mcp_json_from_git() {
let _home = HomeGuard::set();
let cwd_root = tempfile::tempdir().expect("tempdir");
let cwd = cwd_root.path();
let status = std::process::Command::new("git")
.arg("init")
.arg("-q")
.arg(cwd)
.status()
.expect("git must be on PATH to run this test");
assert!(status.success(), "git init failed");
prepare_managed_config("test-session", cwd)
.expect("prepare_managed_config must resolve a config dir under the redirected HOME");
let exclude = std::fs::read_to_string(cwd.join(".git").join("info").join("exclude"))
.expect("git init must create .git/info/exclude");
assert!(
exclude.lines().any(|l| l.trim() == ".mcp.json"),
"prepare_managed_config must git-exclude .mcp.json before the injectors write it \
(#4181) — the resume paths reach this function with no prepare_session; \
exclude file was:\n{exclude}"
);
}
#[serial_test::serial]
#[test]
fn prepare_managed_config_excludes_builtins_when_mcp_json_write_fails() {
let _home = HomeGuard::set();
let cwd_root = tempfile::tempdir().expect("tempdir");
let cwd = cwd_root.path();
std::fs::create_dir_all(cwd.join(".mcp.json")).expect("mkdir .mcp.json");
let config_dir = prepare_managed_config("test-session", cwd).expect(
"prepare_managed_config must still resolve a config dir even when every pin write fails",
);
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(config_dir.join(".claude.json"))
.expect("prepare_managed_config must write <config_dir>/.claude.json"),
)
.expect("<config_dir>/.claude.json must be valid JSON");
let key = cwd.to_string_lossy().to_string();
let enabled: Vec<&str> = value["projects"][&key]["enabledMcpjsonServers"]
.as_array()
.expect("enabledMcpjsonServers is an array")
.iter()
.filter_map(|v| v.as_str())
.collect();
for name in [
"trusty-mpm",
"trusty-review",
"trusty-memory",
"trusty-search",
] {
assert!(
!enabled.contains(&name),
"{name} must not be approved when its pin write failed this run \
(headless resume path, no human to decline a consent dialog): {enabled:?}"
);
}
}
struct PathGuard {
prev: Option<std::ffi::OsString>,
}
impl PathGuard {
fn prepend(dir: &Path) -> Self {
let prev = std::env::var_os("PATH");
let mut entries = vec![dir.to_path_buf()];
if let Some(ref p) = prev {
entries.extend(std::env::split_paths(p));
}
let joined = std::env::join_paths(entries).expect("join PATH");
unsafe { std::env::set_var("PATH", joined) };
Self { prev }
}
}
impl Drop for PathGuard {
fn drop(&mut self) {
match self.prev.take() {
Some(v) => unsafe { std::env::set_var("PATH", v) },
None => unsafe { std::env::remove_var("PATH") },
}
}
}
fn find_claude_json_files(root: &Path) -> Vec<String> {
fn walk(dir: &Path, depth: usize, out: &mut Vec<String>) {
if depth > 8 {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(meta) = std::fs::symlink_metadata(&path) else {
continue;
};
if meta.is_dir() {
walk(&path, depth + 1, out);
} else if entry.file_name() == std::ffi::OsStr::new(".claude.json") {
out.push(path.display().to_string());
}
}
}
let mut out = Vec::new();
walk(root, 0, &mut out);
out
}
#[cfg(unix)]
fn plant_fake_claude(dir: &Path) {
use std::os::unix::fs::PermissionsExt;
let bin = dir.join("claude");
std::fs::write(&bin, b"#!/bin/sh\nexit 0\n").expect("write fake claude");
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755))
.expect("chmod fake claude");
}
#[serial_test::serial]
#[test]
#[cfg(unix)]
fn spawn_resume_trust_seed_stays_within_redirected_home() {
let home = tempfile::tempdir().expect("home tempdir");
let bindir = tempfile::tempdir().expect("bin tempdir");
let workspace = tempfile::tempdir().expect("workspace tempdir");
plant_fake_claude(bindir.path());
let _path = PathGuard::prepend(bindir.path());
let prev_home = std::env::var_os("HOME");
unsafe { std::env::set_var("HOME", home.path()) };
struct RestoreHome(Option<std::ffi::OsString>);
impl Drop for RestoreHome {
fn drop(&mut self) {
match self.0.take() {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
let _home_guard = RestoreHome(prev_home);
let base = crate::core::trusty_tools_config::managed_claude_config_dir()
.expect("managed config dir resolves under the redirected HOME");
assert!(
base.starts_with(home.path()),
"precondition: the resolved managed dir must sit under the redirected \
HOME, else this test is not isolated at all (resolved {})",
base.display()
);
assert!(
ClaudeCodeAdapter::resolve_claude().is_some(),
"fake claude must be resolvable on PATH — otherwise this test is vacuous"
);
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn_resume(
"tmpm-4206",
None,
workspace.path(),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect("spawn_resume must succeed against the fake tmux");
let found = find_claude_json_files(home.path());
assert!(
found.iter().all(|p| Path::new(p).starts_with(home.path())),
"every seeded .claude.json must live under the redirected HOME: {found:?}"
);
let seeded = base.join(".claude.json");
let text = std::fs::read_to_string(&seeded).unwrap_or_else(|e| {
panic!(
".claude.json must be seeded at the resolved managed dir {}: {e}",
seeded.display()
)
});
let val: serde_json::Value = serde_json::from_str(&text).expect("seeded config is valid JSON");
let key = workspace.path().to_string_lossy().to_string();
assert_eq!(
val["projects"][&key]["hasTrustDialogAccepted"],
serde_json::Value::Bool(true),
"the workspace must actually be trust-seeded under the redirected HOME: {text}"
);
}