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 \
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 project,local"),
"isolation flag must still be present with a config dir: {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_is_byte_identical_to_pre_2246() {
let cmd = spawn_command(
Path::new(TEST_CWD),
"claude",
None,
TEST_SESSION_ID,
None,
None,
None,
);
let expected = format!(
"cd '/tmp/ws' && {{ export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; \
env -u ANTHROPIC_API_KEY claude \
--setting-sources project,local --dangerously-skip-permissions; \
echo 'tm: run `tm` to relaunch this session'; }}"
);
assert_eq!(
cmd, expected,
"no-token command must be byte-identical to pre-#2246"
);
}
#[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(
"env -u ANTHROPIC_API_KEY \
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}"
);
}
#[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_is_byte_identical_to_pre_2246() {
let cmd = resume_command(
Path::new(TEST_CWD),
"claude",
None,
Some("abc-123"),
false,
TEST_SESSION_ID,
None,
None,
None,
);
let expected = format!(
"cd '/tmp/ws' && {{ export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; \
env -u ANTHROPIC_API_KEY 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 must be byte-identical to pre-#2246"
);
}
#[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(
"env -u ANTHROPIC_API_KEY \
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("env -u ANTHROPIC_API_KEY /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]
fn build_prompt_file_writes_resolved_prompt_for_project() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = build_prompt_file(tmp.path()).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]
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 \
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"));
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"));
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"));
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:?}"
);
}
#[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")
);
}