use super::super::launch_spec::LaunchSpec;
use super::super::test_helpers::FakeTmux;
use super::*;
fn sent_spec(line: &str) -> LaunchSpec {
let after = line
.split("--launch-spec '")
.nth(1)
.unwrap_or_else(|| panic!("typed line must name a launch spec: {line}"));
let path = after
.split('\'')
.next()
.unwrap_or_else(|| panic!("launch-spec path must be quoted: {line}"));
LaunchSpec::consume(std::path::Path::new(path)).expect("the typed spec must be readable")
}
fn only_line(fake: &FakeTmux) -> String {
let sends = fake.sends.lock().expect("send log");
let pane_sends = fake.pane_sends.lock().expect("pane send log");
match (sends.len(), pane_sends.len()) {
(1, 0) => sends[0].1.clone(),
(0, 1) => pane_sends[0].2.clone(),
_ => panic!("expected exactly one send, got {sends:?} / {pane_sends:?}"),
}
}
fn has_flag_pair(spec: &LaunchSpec, flag: &str, value: &str) -> bool {
spec.args.windows(2).any(|w| w[0] == flag && w[1] == value)
}
const TEST_SESSION_ID: &str = "11111111-2222-3333-4444-555555555555";
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 }
}
fn home(&self) -> &std::path::Path {
self.tmp.path()
}
}
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") },
}
}
}
fn inert_framework_root() -> std::path::PathBuf {
std::env::temp_dir()
.join("tm-inert-root")
.join(".trusty-mpm")
}
#[test]
fn claude_code_adapter_identifies() {
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake, None, &inert_framework_root());
assert_eq!(adapter.identify(), "claude-code");
}
#[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");
}
}
#[test]
fn publish_session_env_sets_id_and_config_dir() {
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone(), None, &inert_framework_root());
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(), None, &inert_framework_root());
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");
}
#[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"))
.0
.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"))
.0
.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"))
.0
.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();
}
fn usage_dir_of(framework_root: &std::path::Path) -> std::path::PathBuf {
framework_root.join("usage")
}
use crate::runtime::prompt_file::build_prompt_file_in;
#[test]
#[serial_test::serial]
fn build_prompt_file_records_under_the_named_framework_root() {
let home = HomeGuard::set();
let root = tempfile::tempdir().expect("named framework root");
let project = tempfile::tempdir().expect("project");
let path = build_prompt_file_in(
root.path(),
&std::env::temp_dir(),
project.path(),
Some("sess-1"),
)
.0
.expect("prompt file written");
std::fs::remove_file(&path).ok();
assert!(
usage_dir_of(root.path()).exists(),
"the fold must be recorded under the named root: {}",
root.path().display()
);
assert!(
!home.home().join(".trusty-mpm").join("usage").exists(),
"the producer wrote under the PROCESS home ({}) instead of the root it \
was given — this is #7568's defect, and on a real machine that home is \
the operator's own",
home.home().display()
);
}
#[test]
#[serial_test::serial]
fn an_ambient_build_prompt_file_records_under_whatever_home_it_inherits() {
let home = HomeGuard::set();
let project = tempfile::tempdir().expect("project");
let path = build_prompt_file(project.path(), Some("sess-1"))
.0
.expect("prompt file written");
std::fs::remove_file(&path).ok();
assert!(
usage_dir_of(&home.home().join(".trusty-mpm")).exists(),
"the ambient form must record under the process's home — if this stops \
holding, the guard above is asserting on something nothing produces \
any more and must be re-pointed (decoy home {})",
home.home().display()
);
}
#[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 = encode_project_dir(&cwd);
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 = encode_project_dir(&cwd);
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 encode_project_dir_folds_dot_in_worktrees_path() {
assert_eq!(
encode_project_dir(Path::new("/private/tmp/tm6777/proj/.worktrees/tm-proj-01")),
"-private-tmp-tm6777-proj--worktrees-tm-proj-01"
);
assert_eq!(
encode_project_dir(Path::new(
"/private/tmp/tm6777/tool/.claude/worktrees/agent-0123456789abcdef0"
)),
"-private-tmp-tm6777-tool--claude-worktrees-agent-0123456789abcdef0"
);
}
#[test]
fn encode_project_dir_folds_every_non_alphanumeric() {
for (raw, expected) in [
("/a.b", "-a-b"),
("/a_b", "-a-b"),
("/a b", "-a-b"),
("/a@b", "-a-b"),
("/a+b", "-a-b"),
("/a~b", "-a-b"),
("/a'b", "-a-b"),
("/a(b)", "-a-b-"),
("/a:b", "-a-b"),
("/a#b", "-a-b"),
("/a%b", "-a-b"),
("/a=b", "-a-b"),
("/a,b", "-a-b"),
("/a-b", "-a-b"),
("/AbZ9", "-AbZ9"),
("/café", "-caf-"),
("/日本", "---"),
] {
assert_eq!(
encode_project_dir(Path::new(raw)),
expected,
"encoding {raw} must fold to {expected}"
);
}
}
#[test]
fn encode_project_dir_folds_astral_char_to_two_dashes() {
assert_eq!(encode_project_dir(Path::new("/emo-🚀x")), "-emo---x");
}
#[test]
fn encode_project_dir_truncates_and_hashes_a_long_path() {
let cwd = "/private/tmp/tm6777/deep/seg00/seg01/seg02/seg03/seg04/seg05/seg06/seg07/seg08/\
seg09/seg10/seg11/seg12/seg13/seg14/seg15/seg16/seg17/seg18/seg19/seg20/seg21/seg22/seg23/seg24/\
seg25/seg26/seg27/seg28/seg29/seg30/seg31/seg32/seg33/seg34/seg35/seg36/seg37/seg38/seg39";
let expected = "-private-tmp-tm6777-deep-seg00-seg01-seg02-seg03-seg04-seg05-seg06-seg07-\
seg08-seg09-seg10-seg11-seg12-seg13-seg14-seg15-seg16-seg17-seg18-seg19-seg20-seg21-seg22-seg23-\
seg24-seg25-seg26-seg27-seg28-s-fy7046";
let encoded = encode_project_dir(Path::new(cwd));
assert_eq!(
encoded, expected,
"an over-length cwd must truncate to 200 chars plus the base36 path hash"
);
assert_eq!(
encoded.len(),
207,
"200 kept characters, one separator, and a 6-character hash"
);
}
#[test]
fn session_id_exists_finds_hardcoded_dir_name_for_dotted_cwd() {
let tmp = tempfile::tempdir().expect("tempdir");
let projects_dir = tmp.path().join("projects");
let project_dir = projects_dir.join("-repo--worktrees-w1");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join("dotted-id.jsonl"), "{}").unwrap();
assert!(
session_id_exists_in(Path::new("/repo/.worktrees/w1"), &projects_dir, "dotted-id"),
"a cwd under .worktrees/ must resolve to the '--worktrees-' directory \
Claude Code really creates"
);
}
#[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"
);
}
#[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 = encode_project_dir(&cwd);
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:?}"
);
}
#[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")
);
}
struct PathGuard {
prev: Option<std::ffi::OsString>,
_env: std::sync::MutexGuard<'static, ()>,
}
impl PathGuard {
fn prepend(dir: &Path) -> Self {
let env = crate::core::trusty_tools_config::env_test_lock();
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, _env: env }
}
}
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(), None, &home.path().join(".trusty-mpm"));
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}"
);
}
#[test]
fn prepare_managed_config_writes_no_mcp_json_and_no_approval() {
let root_dir = tempfile::tempdir().expect("root tempdir");
let fw = crate::core::paths::FrameworkPaths::under(root_dir.path());
let cwd_root = tempfile::tempdir().expect("tempdir");
let cwd = cwd_root.path();
let config_dir = prepare_managed_config_with_exe(
&fw,
"test-session",
cwd,
Some(std::path::Path::new(crate::test_support::STABLE_HOOK_EXE)),
);
assert!(
!cwd.join(".mcp.json").exists(),
"the daemon spawn path must not write a workspace .mcp.json"
);
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 entry = &value["projects"][&key];
assert_eq!(
entry["hasTrustDialogAccepted"],
serde_json::json!(true),
"#1269's trust half still runs: {entry}"
);
assert!(
entry.get("enabledMcpjsonServers").is_none(),
"no MCP name may be pre-approved on the daemon path: {entry}"
);
let servers = value["mcpServers"]
.as_object()
.expect("the user-scope mcpServers map is seeded");
for name in [
"trusty-mpm",
"trusty-review",
"trusty-memory",
"trusty-search",
] {
assert!(
servers.contains_key(name),
"{name} must be declared in user scope: {servers:?}"
);
}
}
#[test]
fn prepare_managed_config_merges_the_project_hook_group() {
let root_dir = tempfile::tempdir().expect("root tempdir");
let fw = crate::core::paths::FrameworkPaths::under(root_dir.path());
let cwd_root = tempfile::tempdir().expect("tempdir");
let cwd = cwd_root.path();
let claude = cwd.join(".claude");
std::fs::create_dir_all(&claude).expect("create .claude");
let settings = claude.join("settings.json");
std::fs::write(
&settings,
serde_json::json!({
"hooks": {
"PreToolUse": [
{ "hooks": [{ "type": "command", "command": "/usr/local/bin/tm hook --pm-guard" }] }
],
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "trusty-memory inbox-check" }] }
]
}
})
.to_string(),
)
.expect("seed settings");
prepare_managed_config_with_exe(
&fw,
"test-session",
cwd,
Some(std::path::Path::new(crate::test_support::STABLE_HOOK_EXE)),
);
let after: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings).expect("read settings"))
.expect("settings is valid JSON");
let session_start = after["hooks"]["SessionStart"].to_string();
assert!(
session_start.contains(&format!("{} hook\"", crate::test_support::STABLE_HOOK_EXE)),
"every session-composition path must merge the SessionStart tm-hook group: {session_start}"
);
assert!(
session_start.contains("trusty-memory inbox-check"),
"the project's own SessionStart entry must survive: {session_start}"
);
}
#[test]
fn prepare_managed_config_writes_only_under_the_named_framework_root() {
let root_dir = tempfile::tempdir().expect("root tempdir");
let base = root_dir.path();
let fw = crate::core::paths::FrameworkPaths::under(base);
let cwd_root = tempfile::tempdir().expect("cwd tempdir");
let config_dir = prepare_managed_config_with_exe(
&fw,
"test-session",
cwd_root.path(),
Some(std::path::Path::new(crate::test_support::STABLE_HOOK_EXE)),
);
assert!(
config_dir.starts_with(base),
"the managed CLAUDE_CONFIG_DIR must sit under the named root, got {}",
config_dir.display()
);
assert!(
config_dir.join(".claude.json").is_file(),
"the provisioning must actually have run there, not merely named it"
);
assert!(
fw.root.starts_with(base) && config_dir.join("agents").is_dir(),
"the roster deploy must land under the named root: {}",
fw.root.display()
);
}
#[test]
fn spawn_writes_its_launch_spec_under_the_named_framework_root() {
let root_dir = tempfile::tempdir().expect("root tempdir");
let root = root_dir.path().join(".trusty-mpm");
let adapter = ClaudeCodeAdapter::new(FakeTmux::new(), None, &root);
let dir = adapter.spec_dir();
assert!(
dir.starts_with(root_dir.path()),
"launch specs must not leave the named root: {}",
dir.display()
);
assert!(dir.ends_with("launch-specs"), "{}", dir.display());
}
#[serial_test::serial]
#[test]
fn compose_inplace_args_never_continues_from_home_store() {
let _home = HomeGuard::set();
let home = dirs::home_dir().expect("home resolves under redirected HOME");
let cwd = std::path::PathBuf::from("/tmp/inplace-6765-test");
let encoded = encode_project_dir(&cwd);
let home_project_dir = home.join(".claude").join("projects").join(&encoded);
std::fs::create_dir_all(&home_project_dir).unwrap();
std::fs::write(home_project_dir.join("some-other-session.jsonl"), "{}").unwrap();
let config_dir = home.join("managed-config-6765");
std::fs::create_dir_all(config_dir.join("projects")).unwrap();
let args = compose_inplace_args(&cwd, Some(&config_dir), None, None);
assert!(
!args.contains(&"--continue".to_owned()),
"a null claude_session_id must never emit a bare --continue: {args:?}"
);
assert!(
!args.contains(&"--resume".to_owned()),
"a null claude_session_id must not emit --resume either: {args:?}"
);
let args = compose_inplace_args(&cwd, Some(&config_dir), Some("stale-id-6765"), None);
assert!(
!args.contains(&"--continue".to_owned()),
"a stale id must fall back to a fresh launch, not --continue: {args:?}"
);
assert!(
!args.contains(&"--resume".to_owned()),
"a stale id must not be passed to --resume: {args:?}"
);
let managed_project_dir = config_dir.join("projects").join(&encoded);
std::fs::create_dir_all(&managed_project_dir).unwrap();
std::fs::write(managed_project_dir.join("live-id-6765.jsonl"), "{}").unwrap();
let args = compose_inplace_args(&cwd, Some(&config_dir), Some("live-id-6765"), None);
assert!(
args.windows(2).any(|w| w == ["--resume", "live-id-6765"]),
"an id present in the session's own store must resume by id: {args:?}"
);
assert!(
!args.contains(&"--continue".to_owned()),
"--resume must never be paired with --continue: {args:?}"
);
}
fn drive_spawn(fake: &std::sync::Arc<FakeTmux>, home: &HomeGuard) -> String {
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter
.spawn(
"tm-sess",
home.home(),
"task",
TEST_SESSION_ID,
&[("GH_TOKEN".to_owned(), "gho_fake".to_owned())],
)
.expect("spawn must succeed with claude on PATH");
only_line(fake)
}
#[test]
#[serial_test::serial]
fn spawn_sends_the_parameterized_launch_line() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let line = drive_spawn(&fake, &home);
assert!(
line.len() < 512,
"the typed line must stay far under MAX_CANON; got {} for {line}",
line.len()
);
assert!(
line.contains("internal-spawn-disclaimed --launch-spec '"),
"#2997: claude must still be spawned by the disclaim shim: {line}"
);
assert!(
!line.contains("--dangerously-skip-permissions"),
"no launch flag may appear in the typed line: {line}"
);
let spec = sent_spec(&line);
assert_eq!(spec.session_id, TEST_SESSION_ID);
assert_eq!(spec.cwd, home.home());
}
#[test]
#[serial_test::serial]
fn spawn_sends_env_scrub_when_binary_available() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let spec = sent_spec(&drive_spawn(&fake, &home));
assert!(
spec.env_unset.contains(&"ANTHROPIC_API_KEY".to_owned()),
"DOC-34: the API key must be stripped: {:?}",
spec.env_unset
);
for marker in crate::core::claude_env_scrub::INHERITED_SESSION_MARKERS {
assert!(
spec.env_unset.iter().any(|n| n == marker),
"#4467: {marker} must be scrubbed: {:?}",
spec.env_unset
);
}
assert!(
!spec.env_unset.iter().any(|n| n == "CLAUDE_CONFIG_DIR"),
"#4451/#4455: the relocation must never be scrubbed: {:?}",
spec.env_unset
);
}
#[test]
#[serial_test::serial]
fn spawn_keeps_the_gh_token_out_of_the_typed_line() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let line = drive_spawn(&fake, &home);
assert!(
!line.contains("gho_fake"),
"no secret may be typed into the pane: {line}"
);
let spec = sent_spec(&line);
assert!(
spec.env_set
.iter()
.any(|(k, v)| k == "GH_TOKEN" && v == "gho_fake"),
"the pinned identity must still reach claude: {:?}",
spec.env_set
);
}
#[test]
#[serial_test::serial]
fn spawn_errors_when_the_line_is_refused() {
use crate::session_manager::{ManagedError, ManagedTmuxDriver};
struct Refusing;
impl ManagedTmuxDriver for Refusing {
fn create_session(&self, _n: &str, _w: &str) -> Result<(), ManagedError> {
Ok(())
}
fn kill_session(&self, _n: &str) -> Result<(), ManagedError> {
Ok(())
}
fn send_line(&self, _n: &str, _t: &str) -> Result<(), ManagedError> {
Err(ManagedError::TmuxUnavailable("refused".into()))
}
fn capture(&self, _n: &str, _l: usize) -> Result<String, ManagedError> {
Ok(String::new())
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
Ok(vec!["tm-sess".to_owned()])
}
}
let home = HomeGuard::set();
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let adapter = ClaudeCodeAdapter::new(
std::sync::Arc::new(Refusing),
Some(true),
&home.home().join(".trusty-mpm"),
);
let err = adapter
.spawn("tm-sess", home.home(), "task", TEST_SESSION_ID, &[])
.expect_err("a refused send must not report a successful spawn");
assert!(matches!(err, RuntimeError::Spawn(_)), "{err}");
assert!(
err.to_string().contains("refusing to launch into pane"),
"the refusal must name the pane it declined: {err}"
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_with_id_uses_resume_flag() {
let home = HomeGuard::set();
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let cwd = home.home().join("ws");
std::fs::create_dir_all(&cwd).expect("mkdir ws");
let config_dir = crate::core::trusty_tools_config::managed_claude_config_dir()
.expect("a redirected HOME must resolve");
let projects = config_dir.join("projects").join(encode_project_dir(&cwd));
std::fs::create_dir_all(&projects).expect("mkdir projects");
std::fs::write(projects.join("conv-77.jsonl"), b"{}").expect("seed conversation");
let fake = FakeTmux::new();
let adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter
.spawn_resume(
"tm-sess",
Some("%3"),
&cwd,
"task",
Some("conv-77"),
TEST_SESSION_ID,
&[],
)
.expect("resume must succeed");
let pane_sends = fake.pane_sends.lock().expect("pane send log").clone();
assert_eq!(
pane_sends.len(),
1,
"#2456: a known pane_id must be targeted directly: {pane_sends:?}"
);
assert_eq!(pane_sends[0].1, "%3");
let spec = sent_spec(&pane_sends[0].2);
assert!(
has_flag_pair(&spec, "--resume", "conv-77"),
"a live id must resume by id: {:?}",
spec.args
);
assert!(
!spec.args.iter().any(|a| a == "--continue"),
"#6765: never a bare --continue: {:?}",
spec.args
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_falls_back_to_session_target_when_pane_id_unknown() {
let home = HomeGuard::set();
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let fake = FakeTmux::new();
let adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter
.spawn_resume(
"tm-sess",
None,
home.home(),
"task",
Some("no-such-conversation"),
TEST_SESSION_ID,
&[],
)
.expect("a stale id must still launch");
let sends = fake.sends.lock().expect("send log").clone();
assert_eq!(
sends.len(),
1,
"no pane id recorded → the session-scoped send: {sends:?}"
);
let spec = sent_spec(&sends[0].1);
assert!(
!spec
.args
.iter()
.any(|a| a == "--resume" || a == "--continue"),
"a stale id must launch fresh: {:?}",
spec.args
);
assert!(
spec.args
.iter()
.any(|a| a == crate::core::model_inject::PERMISSION_MODE_FLAG),
"a fresh launch still carries the isolation flags: {:?}",
spec.args
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_sends_prompt_file_when_binary_available() {
let home = HomeGuard::set();
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let fake = FakeTmux::new();
let adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter
.spawn_resume(
"tm-sess",
None,
home.home(),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect("resume must succeed");
let spec = sent_spec(&only_line(&fake));
let flag = spec
.args
.iter()
.position(|a| a == "--append-system-prompt-file")
.expect("the resume must carry the PM prompt");
assert!(
!spec.args[flag + 1].contains('\''),
"the path is an argv token, never shell-quoted: {:?}",
spec.args
);
}
#[test]
#[serial_test::serial]
fn spawn_uses_the_launch_resolved_reachability() {
let home = HomeGuard::set();
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let fake = FakeTmux::new();
let adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(false), &home.home().join(".trusty-mpm"));
adapter
.spawn("tm-sess", home.home(), "task", TEST_SESSION_ID, &[])
.expect("spawn must succeed");
let spec = sent_spec(&only_line(&fake));
assert!(
!spec
.env_set
.iter()
.any(|(k, _)| k == "CLAUDE_CODE_DISABLE_AUTO_MEMORY"),
"an unreachable trusty-memory must leave auto memory on: {:?}",
spec.env_set
);
}
fn write_state_config(home: &HomeGuard, yaml: &str) {
let root = home.home().join(".trusty-tools").join("trusty-mpm");
std::fs::create_dir_all(&root).expect("mkdir state root");
std::fs::write(root.join("config.yaml"), yaml).expect("write config");
}
#[test]
#[serial_test::serial]
fn spawn_carries_the_configured_fullscreen_renderer() {
use crate::core::alt_screen::{ALT_SCREEN_ENABLED, ALT_SCREEN_ENV_VAR};
let home = HomeGuard::set();
write_state_config(&home, "tmux:\n alternate_screen: true\n");
let spawned = FakeTmux::new();
let spawn_spec = sent_spec(&drive_spawn(&spawned, &home));
let resumed = FakeTmux::new();
drive_resume(&resumed, &home, home.home(), None, None);
let resume_spec = sent_spec(&only_line(&resumed));
for spec in [spawn_spec, resume_spec] {
assert!(
spec.env_set
.iter()
.any(|(k, v)| k == ALT_SCREEN_ENV_VAR && v == ALT_SCREEN_ENABLED),
"the configured renderer must ride in the spec: {:?}",
spec.env_set
);
}
}
#[test]
#[serial_test::serial]
fn spawn_falls_back_with_the_tmux_option_on_an_unreadable_config() {
use tracing_subscriber::layer::SubscriberExt;
let home = HomeGuard::set();
write_state_config(&home, "tmux:\n alternate_screen: [broken\n");
let tmux_option = crate::core::trusty_tools_config::resolve_tmux_options(
&crate::core::trusty_tools_config::TrustyToolsConfig::load(),
)
.alternate_screen;
let want = crate::core::alt_screen::configured_env(tmux_option);
crate::test_support::enable_event_capture();
let buffer = trusty_common::log_buffer::LogBuffer::new(256);
let subscriber = tracing_subscriber::registry().with(
trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
);
let (spawned, resumed) = (FakeTmux::new(), FakeTmux::new());
tracing::subscriber::with_default(subscriber, || {
drive_spawn(&spawned, &home);
drive_resume(&resumed, &home, home.home(), None, None);
});
for spec in [
sent_spec(&only_line(&spawned)),
sent_spec(&only_line(&resumed)),
] {
for pair in &want {
assert!(
spec.env_set.contains(pair),
"the renderer must match the tmux option's fallback {pair:?}: {:?}",
spec.env_set
);
}
}
let logged = buffer.tail(256).join("\n");
let config = home.home().join(".trusty-tools/trusty-mpm/config.yaml");
assert!(
logged.contains("alternate_screen") && logged.contains(&config.display().to_string()),
"the warning must name the file: {logged}"
);
}
#[test]
#[serial_test::serial]
fn spawn_publishes_session_id_via_set_environment() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let line = drive_spawn(&fake, &home);
assert!(
line.starts_with(&format!(
"export {}='{TEST_SESSION_ID}'; ",
crate::core::harness_root::MANAGED_SESSION_ID_ENV
)),
"#2023 B: the export must land in the PANE's own shell: {line}"
);
let env_sets = fake.env_sets.lock().expect("env-set log").clone();
assert!(
env_sets
.iter()
.any(|(_, k, v)| k == "TM_MANAGED_SESSION_ID" && v == TEST_SESSION_ID),
"the durable publish must still happen: {env_sets:?}"
);
}
fn drive_resume(
fake: &std::sync::Arc<FakeTmux>,
home: &HomeGuard,
cwd: &std::path::Path,
pane_id: Option<&str>,
claude_session_id: Option<&str>,
) {
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter
.spawn_resume(
"tm-sess",
pane_id,
cwd,
"task",
claude_session_id,
TEST_SESSION_ID,
&[],
)
.expect("resume must succeed with claude on PATH");
}
#[test]
#[serial_test::serial]
fn spawn_resume_targets_stored_pane_id_when_known() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let cwd = home.home().to_path_buf();
drive_resume(&fake, &home, &cwd, Some("%9"), None);
assert!(
fake.sends.lock().expect("send log").is_empty(),
"a known pane id must never take the session-scoped path"
);
let pane_sends = fake.pane_sends.lock().expect("pane send log").clone();
assert_eq!(pane_sends.len(), 1);
assert_eq!(pane_sends[0].1, "%9");
}
#[test]
#[serial_test::serial]
fn spawn_interrupts_the_sessions_own_pane_never_the_active_one() {
use crate::session_manager::{ManagedError, ManagedTmuxDriver};
#[derive(Default)]
struct PaneAware {
session_interrupts: std::sync::Mutex<Vec<String>>,
pane_interrupts: std::sync::Mutex<Vec<String>>,
printed: std::sync::Mutex<String>,
freed: std::sync::Mutex<bool>,
}
impl PaneAware {
fn run(&self, text: &str) -> bool {
let Some(word) = text.strip_prefix("echo ") else {
return false;
};
if !*self.freed.lock().expect("freed") {
return true;
}
let mut out = self.printed.lock().expect("printed");
out.push_str(&word.replace(['"', '\''], ""));
out.push('\n');
true
}
}
impl ManagedTmuxDriver for PaneAware {
fn create_session(&self, _n: &str, _w: &str) -> Result<(), ManagedError> {
Ok(())
}
fn kill_session(&self, _n: &str) -> Result<(), ManagedError> {
Ok(())
}
fn send_line(&self, _n: &str, text: &str) -> Result<(), ManagedError> {
self.run(text);
Ok(())
}
fn send_line_to_pane(&self, _n: &str, _p: &str, text: &str) -> Result<(), ManagedError> {
self.run(text);
Ok(())
}
fn send_interrupt(&self, name: &str) -> Result<(), ManagedError> {
self.session_interrupts
.lock()
.expect("log")
.push(name.to_owned());
*self.freed.lock().expect("freed") = true;
Ok(())
}
fn send_interrupt_to_pane(&self, _n: &str, pane: &str) -> Result<(), ManagedError> {
self.pane_interrupts
.lock()
.expect("log")
.push(pane.to_owned());
*self.freed.lock().expect("freed") = true;
Ok(())
}
fn get_pane_id(&self, _name: &str) -> Option<String> {
Some("%3".to_owned())
}
fn capture(&self, _n: &str, _l: usize) -> Result<String, ManagedError> {
if *self.freed.lock().expect("freed") {
Ok(self.printed.lock().expect("printed").clone())
} else {
Ok("quote>".to_owned())
}
}
fn capture_pane(&self, n: &str, _p: &str, l: usize) -> Result<String, ManagedError> {
self.capture(n, l)
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
Ok(vec!["tm-sess".to_owned()])
}
}
let home = HomeGuard::set();
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let _path = PathGuard::prepend(&bin_dir);
let tmux = std::sync::Arc::new(PaneAware::default());
let adapter =
ClaudeCodeAdapter::new(tmux.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter
.spawn("tm-sess", home.home(), "task", TEST_SESSION_ID, &[])
.expect("the spawn must reach the pane");
assert_eq!(
tmux.pane_interrupts.lock().expect("log").clone(),
vec!["%3".to_owned()],
"the interrupt must go to the session's OWN pane, resolved from the driver"
);
assert!(
tmux.session_interrupts.lock().expect("log").is_empty(),
"a session-scoped interrupt lands in whichever pane is active — the \
operator's, on a spawn into an existing session"
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_never_sends_bare_continue() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let cwd = home.home().to_path_buf();
drive_resume(&fake, &home, &cwd, None, None);
let spec = sent_spec(&only_line(&fake));
assert!(
!spec.args.iter().any(|a| a == "--continue"),
"never a bare --continue: {:?}",
spec.args
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_without_id_no_prior_conv_sends_plain_spawn() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let cwd = home.home().to_path_buf();
drive_resume(&fake, &home, &cwd, None, None);
let spec = sent_spec(&only_line(&fake));
assert!(
!spec.args.iter().any(|a| a == "--resume" || a == "attach"),
"a plain spawn carries no resume selection: {:?}",
spec.args
);
assert!(
spec.args
.iter()
.any(|a| a == crate::core::model_inject::PERMISSION_MODE_FLAG),
"and it still carries the isolation flags: {:?}",
spec.args
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_drops_a_stale_stored_id() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let cwd = home.home().to_path_buf();
drive_resume(&fake, &home, &cwd, None, Some("pruned-conversation"));
let spec = sent_spec(&only_line(&fake));
assert!(
!spec.args.iter().any(|a| a == "--resume"),
"a stale id must not reach `claude --resume`, which fails hard: {:?}",
spec.args
);
assert!(
!spec.args.iter().any(|a| a == "pruned-conversation"),
"and the id itself must appear nowhere in the argv: {:?}",
spec.args
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_sends_oauth_token_when_available() {
let home = HomeGuard::set();
unsafe { std::env::set_var("CLAUDE_CODE_OAUTH_TOKEN", "sk-ant-oat01-test") };
let fake = FakeTmux::new();
let cwd = home.home().to_path_buf();
drive_resume(&fake, &home, &cwd, None, None);
let line = only_line(&fake);
unsafe { std::env::remove_var("CLAUDE_CODE_OAUTH_TOKEN") };
assert!(
!line.contains("sk-ant-oat01-test"),
"no token may be typed into the pane: {line}"
);
let spec = sent_spec(&line);
assert!(
spec.env_set.iter().any(|(k, v)| {
k == crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR && v == "sk-ant-oat01-test"
}),
"the token must reach claude's environment: {:?}",
spec.env_set
);
}
#[test]
#[serial_test::serial]
fn spawn_resume_uses_resume_flag_for_a_worktree_cwd() {
let home = HomeGuard::set();
let cwd = home.home().join("repo/.claude/worktrees/agent-1");
std::fs::create_dir_all(&cwd).expect("mkdir worktree");
let config_dir = crate::core::trusty_tools_config::managed_claude_config_dir()
.expect("a redirected HOME must resolve");
let projects = config_dir.join("projects").join(encode_project_dir(&cwd));
std::fs::create_dir_all(&projects).expect("mkdir projects");
std::fs::write(projects.join("conv-wt.jsonl"), b"{}").expect("seed conversation");
let fake = FakeTmux::new();
drive_resume(&fake, &home, &cwd, None, Some("conv-wt"));
let spec = sent_spec(&only_line(&fake));
assert!(
has_flag_pair(&spec, "--resume", "conv-wt"),
"a dotted worktree cwd must still resolve its own transcript: {:?}",
spec.args
);
}
fn adapter_with_unwritable_prompt_dir(
fake: &std::sync::Arc<FakeTmux>,
home: &HomeGuard,
) -> (ClaudeCodeAdapter, std::path::PathBuf, PathGuard) {
let bin_dir = home.home().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
plant_fake_claude(&bin_dir);
let path = PathGuard::prepend(&bin_dir);
let not_a_dir = home.home().join("not-a-dir");
std::fs::write(¬_a_dir, "").expect("plant a file where the prompt dir goes");
let mut adapter =
ClaudeCodeAdapter::new(fake.clone(), Some(true), &home.home().join(".trusty-mpm"));
adapter.prompt_dir = not_a_dir.clone();
(adapter, not_a_dir, path)
}
fn assert_refused(err: RuntimeError, prompt_dir: &Path, fake: &FakeTmux) {
let text = err.to_string();
assert!(
text.contains(&*prompt_dir.to_string_lossy()) && text.contains("os error"),
"the refusal must name the prompt file and the I/O cause: {text}"
);
assert!(
fake.sends.lock().expect("send log").is_empty()
&& fake.pane_sends.lock().expect("pane send log").is_empty(),
"a refused launch must type nothing into the pane"
);
}
#[test]
#[serial_test::serial]
fn spawn_refuses_when_the_prompt_file_cannot_be_written() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let (adapter, not_a_dir, _path) = adapter_with_unwritable_prompt_dir(&fake, &home);
let err = adapter
.spawn("tm-sess", home.home(), "task", TEST_SESSION_ID, &[])
.expect_err("a PM spawn without its prompt must be refused");
assert_refused(err, ¬_a_dir, &fake);
}
#[test]
#[serial_test::serial]
fn spawn_resume_refuses_when_the_prompt_file_cannot_be_written() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let (adapter, not_a_dir, _path) = adapter_with_unwritable_prompt_dir(&fake, &home);
let err = adapter
.spawn_resume(
"tm-sess",
None,
home.home(),
"task",
None,
TEST_SESSION_ID,
&[],
)
.expect_err("a PM resume without its prompt must be refused");
assert_refused(err, ¬_a_dir, &fake);
}
#[test]
#[serial_test::serial]
fn inplace_resume_command_refuses_when_the_prompt_file_cannot_be_written() {
let home = HomeGuard::set();
let fake = FakeTmux::new();
let (_adapter, not_a_dir, _path) = adapter_with_unwritable_prompt_dir(&fake, &home);
let fw = FrameworkPaths::from_root(home.home().join(".trusty-mpm"));
let err = build_inplace_resume_command_with(&fw, ¬_a_dir, home.home(), None)
.map(|_| ())
.expect_err("an in-place PM relaunch without its prompt must be refused");
assert_refused(err, ¬_a_dir, &fake);
}