use std::path::{Path, PathBuf};
use crate::core::config::MpmConfig;
use crate::core::delegation_authority::AgentSummary;
#[derive(Debug, thiserror::Error)]
#[error("could not write the PM system-prompt file {}: {cause}", path.display())]
pub struct PromptFileError {
pub path: PathBuf,
pub cause: std::io::Error,
}
pub fn write_prompt_file_in(dir: &Path, prompt: &str) -> Result<PathBuf, PromptFileError> {
let path = dir.join(format!(
"trusty-mpm-system-prompt-{}.txt",
uuid::Uuid::new_v4()
));
match write_new_private(&path, prompt) {
Ok(()) => Ok(path),
Err(cause) => Err(PromptFileError { path, cause }),
}
}
fn write_new_private(path: &Path, text: &str) -> std::io::Result<()> {
use std::io::Write as _;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
opts.mode(0o600);
}
opts.open(path)?.write_all(text.as_bytes())
}
pub fn write_pm_prompt_file_in(
dir: &Path,
prompt: &str,
project: &Path,
action: &str,
) -> anyhow::Result<PathBuf> {
write_prompt_file_in(dir, prompt).map_err(|err| {
anyhow::anyhow!(
"{err}; refusing to {action} {} without its PM instructions (#8286)",
project.display()
)
})
}
pub fn resolve_pm_model(config: &MpmConfig, explicit: Option<&str>) -> String {
crate::core::config::resolve_agent_model(config, "pm", None, explicit)
}
pub const SETTING_SOURCES_FLAG: &str = "--setting-sources project,local";
pub const SETTING_SOURCES_FLAG_RELOCATED: &str = "--setting-sources user,project,local";
pub fn setting_sources_flag(config_dir: Option<&Path>) -> &'static str {
if config_dir.is_some() {
SETTING_SOURCES_FLAG_RELOCATED
} else {
SETTING_SOURCES_FLAG
}
}
pub fn setting_source_tiers_of(flag: &'static str) -> Vec<&'static str> {
flag.split_once(' ')
.map(|(_, list)| list.split(',').map(str::trim).collect())
.unwrap_or_default()
}
pub fn setting_source_tiers() -> Vec<&'static str> {
setting_source_tiers_of(SETTING_SOURCES_FLAG)
}
pub fn relocated_setting_source_tiers() -> Vec<&'static str> {
setting_source_tiers_of(SETTING_SOURCES_FLAG_RELOCATED)
}
pub fn settings_tier_of(claude_home: &Path, harness_cwd: &Path) -> &'static str {
if claude_home == harness_cwd {
"project"
} else {
"user"
}
}
pub const PERMISSION_MODE_FLAG: &str = "--dangerously-skip-permissions";
pub fn build_claude_command_configured(
model: Option<&str>,
prompt_file: Option<&Path>,
config_dir: Option<&Path>,
mcp_env: &[(String, String)],
scoped_mcp: Option<&Path>,
alternate_screen: bool,
) -> String {
let token = config_dir.and_then(|_| crate::core::oauth_token::resolve_oauth_token());
build_claude_command_with_configured(
model,
prompt_file,
config_dir,
token.as_deref(),
mcp_env,
scoped_mcp,
alternate_screen,
)
}
pub fn build_claude_command_with_configured(
model: Option<&str>,
prompt_file: Option<&Path>,
config_dir: Option<&Path>,
oauth_token: Option<&str>,
mcp_env: &[(String, String)],
scoped_mcp: Option<&Path>,
alternate_screen: bool,
) -> String {
use crate::core::spawn_disclaim::pane::shell_single_quote;
let mut cmd = format!("env{}", crate::core::claude_env_scrub::env_unset_flags());
cmd.push(' ');
cmd.push_str(&crate::core::alt_screen::configured_shell_assignments(
alternate_screen,
));
if let Some(dir) = config_dir {
cmd.push_str(" CLAUDE_CONFIG_DIR=");
cmd.push_str(&shell_single_quote(&dir.display().to_string()));
}
if let Some(token) = oauth_token {
cmd.push(' ');
cmd.push_str(crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR);
cmd.push('=');
cmd.push_str(&shell_single_quote(token));
}
for (name, value) in mcp_env {
cmd.push(' ');
cmd.push_str(name);
cmd.push('=');
cmd.push_str(&shell_single_quote(value));
}
cmd.push_str(" claude");
if let Some(m) = model {
cmd.push_str(" --model ");
cmd.push_str(m);
}
if let Some(p) = prompt_file {
cmd.push_str(" --append-system-prompt-file ");
cmd.push_str(&p.display().to_string());
}
cmd.push(' ');
cmd.push_str(setting_sources_flag(config_dir));
cmd.push_str(&crate::core::session_mcp_scope::mcp_config_flag_string(
scoped_mcp,
));
cmd.push(' ');
cmd.push_str(PERMISSION_MODE_FLAG);
cmd
}
pub fn build_inplace_session_command_configured(alternate_screen: bool) -> String {
inplace_session_line(None, alternate_screen)
}
pub fn build_inplace_session_command_with_prompt(
prompt_file: &Path,
alternate_screen: bool,
) -> String {
inplace_session_line(Some(prompt_file), alternate_screen)
}
fn inplace_session_line(prompt_file: Option<&Path>, alternate_screen: bool) -> String {
let prompt = prompt_file
.map(|p| {
format!(
" --append-system-prompt-file {}",
crate::core::spawn_disclaim::pane::shell_single_quote(&p.display().to_string())
)
})
.unwrap_or_default();
format!(
"env{} {} claude{prompt} {}",
crate::core::claude_env_scrub::env_unset_flags(),
crate::core::alt_screen::configured_shell_assignments(alternate_screen),
PERMISSION_MODE_FLAG
)
}
pub fn build_client_session_command_configured(
prompt_file: Option<&Path>,
alternate_screen: bool,
) -> String {
let mut cmd = format!(
"env{} {} claude",
crate::core::claude_env_scrub::env_unset_flags(),
crate::core::alt_screen::configured_shell_assignments(alternate_screen)
);
if let Some(p) = prompt_file {
cmd.push_str(" --append-system-prompt-file ");
cmd.push_str(&p.display().to_string());
}
cmd
}
pub fn build_agent_command_configured(
config: &MpmConfig,
agent: &AgentSummary,
prompt_file: Option<&Path>,
explicit: Option<&str>,
alternate_screen: bool,
) -> String {
let model = crate::core::config::resolve_agent_model(
config,
&agent.name,
agent.model.as_deref(),
explicit,
);
build_claude_command_configured(Some(&model), prompt_file, None, &[], None, alternate_screen)
}
pub fn build_claude_command(
model: Option<&str>,
prompt_file: Option<&Path>,
config_dir: Option<&Path>,
mcp_env: &[(String, String)],
scoped_mcp: Option<&Path>,
) -> String {
build_claude_command_configured(
model,
prompt_file,
config_dir,
mcp_env,
scoped_mcp,
crate::core::alt_screen::configured_alternate_screen(),
)
}
pub fn build_claude_command_with(
model: Option<&str>,
prompt_file: Option<&Path>,
config_dir: Option<&Path>,
oauth_token: Option<&str>,
mcp_env: &[(String, String)],
scoped_mcp: Option<&Path>,
) -> String {
build_claude_command_with_configured(
model,
prompt_file,
config_dir,
oauth_token,
mcp_env,
scoped_mcp,
crate::core::alt_screen::configured_alternate_screen(),
)
}
pub fn build_inplace_session_command() -> String {
build_inplace_session_command_configured(crate::core::alt_screen::configured_alternate_screen())
}
pub fn build_client_session_command(prompt_file: Option<&Path>) -> String {
build_client_session_command_configured(
prompt_file,
crate::core::alt_screen::configured_alternate_screen(),
)
}
pub fn build_agent_command(
config: &MpmConfig,
agent: &AgentSummary,
prompt_file: Option<&Path>,
explicit: Option<&str>,
) -> String {
build_agent_command_configured(
config,
agent,
prompt_file,
explicit,
crate::core::alt_screen::configured_alternate_screen(),
)
}
#[cfg(test)]
mod tests {
use super::*;
const FLAGS: &str = "--setting-sources project,local --dangerously-skip-permissions";
fn head() -> String {
format!(
"env{} {} claude",
crate::core::claude_env_scrub::env_unset_flags(),
crate::core::alt_screen::configured_shell_assignments(false)
)
}
#[test]
fn claude_command_bare() {
assert_eq!(
build_claude_command_configured(None, None, None, &[], None, false),
format!("{} {FLAGS}", head())
);
}
#[test]
fn claude_command_with_model() {
let cmd =
build_claude_command_configured(Some("claude-opus-4-5"), None, None, &[], None, false);
assert_eq!(cmd, format!("{} --model claude-opus-4-5 {FLAGS}", head()));
}
#[test]
fn claude_command_scrubs_inherited_session_markers() {
let cmd = build_claude_command_configured(None, None, None, &[], None, false);
assert!(
cmd.starts_with("env -u "),
"the launch line must carry an env scrub prefix: {cmd}"
);
for marker in [
"CLAUDE_CODE_CHILD_SESSION",
"CLAUDE_CODE_SESSION_ID",
"CLAUDECODE",
"CLAUDE_PID",
"CLAUDE_EFFORT",
"CLAUDE_CODE_EXECPATH",
] {
assert!(
cmd.contains(&format!("-u {marker}")),
"the launch line must unset {marker}: {cmd}"
);
}
assert!(
!cmd.contains("CLAUDE_CONFIG_DIR"),
"this builder must not touch CLAUDE_CONFIG_DIR at all: {cmd}"
);
assert!(
!cmd.contains("-u CLAUDE_CODE_OAUTH_TOKEN"),
"must never unset the OAuth token (#2246): {cmd}"
);
}
#[test]
fn claude_command_relocated_never_scrubs_what_it_assigns() {
let dir = Path::new("/tm/claude-config");
let cmd = build_claude_command_with_configured(
None,
None,
Some(dir),
Some("tok"),
&[],
None,
false,
);
for name in crate::core::claude_env_scrub::DELIBERATE_SPAWN_ENV {
assert!(
cmd.contains(&format!(" {name}=")),
"the relocated line must ASSIGN {name}: {cmd}"
);
assert!(
!cmd.contains(&format!("-u {name}")),
"the relocated line must never unset {name} (#4455/#2246): {cmd}"
);
}
}
#[test]
fn claude_command_relocates_the_config_dir() {
let dir = Path::new("/tm/claude-config");
let cmd = build_claude_command_with_configured(
None,
None,
Some(dir),
Some("tok-abc"),
&[],
None,
false,
);
assert_eq!(
cmd,
format!(
"env{} {} CLAUDE_CONFIG_DIR='/tm/claude-config' \
CLAUDE_CODE_OAUTH_TOKEN='tok-abc' claude \
--setting-sources user,project,local --dangerously-skip-permissions",
crate::core::claude_env_scrub::env_unset_flags(),
crate::core::alt_screen::configured_shell_assignments(false)
)
);
let first_assignment = cmd.find('=').expect("the line carries an assignment");
let last_unset = cmd.rfind("-u ").expect("the line carries scrub flags");
assert!(
last_unset < first_assignment,
"every -u flag must precede every NAME=VALUE assignment: {cmd}"
);
}
#[test]
fn claude_command_omits_the_oauth_token_when_absent() {
let dir = Path::new("/tm/claude-config");
let cmd =
build_claude_command_with_configured(None, None, Some(dir), None, &[], None, false);
assert!(
cmd.contains("CLAUDE_CONFIG_DIR='/tm/claude-config'"),
"the config dir must still be relocated: {cmd}"
);
assert!(
!cmd.contains("CLAUDE_CODE_OAUTH_TOKEN"),
"no token resolved → no assignment at all: {cmd}"
);
assert!(
cmd.contains("--setting-sources user,project,local"),
"the tier list follows the config dir, not the token: {cmd}"
);
}
#[test]
fn claude_command_quotes_a_config_dir_with_a_space() {
let dir = Path::new("/Users/John Doe/.trusty-tools/claude-config");
let cmd =
build_claude_command_with_configured(None, None, Some(dir), None, &[], None, false);
assert!(
cmd.contains("CLAUDE_CONFIG_DIR='/Users/John Doe/.trusty-tools/claude-config'"),
"the config dir must be single-quoted: {cmd}"
);
}
#[test]
fn claude_command_carries_a_non_empty_mcp_env() {
let mcp_env = vec![
(
"TRUSTY_MEMORY_PALACE".to_owned(),
"owner repo slug".to_owned(),
),
("TRUSTY_INDEX".to_owned(), "idx-42".to_owned()),
];
let cmd = build_claude_command_with_configured(
None,
None,
Some(Path::new("/tm/claude-config")),
None,
&mcp_env,
None,
false,
);
assert!(
cmd.contains(" TRUSTY_MEMORY_PALACE='owner repo slug'"),
"the palace pin must be assigned and single-quoted: {cmd}"
);
assert!(
cmd.contains(" TRUSTY_INDEX='idx-42'"),
"the index pin must be assigned and single-quoted: {cmd}"
);
assert!(
cmd.find("TRUSTY_INDEX=").unwrap() < cmd.find(" claude").unwrap(),
"both pins must precede the binary, or they are argv rather than env: {cmd}"
);
}
#[test]
fn claude_command_relocated_isolates_by_relocation_not_exclusion() {
let dir = Path::new("/tm/claude-config");
let cmd =
build_claude_command_with_configured(None, None, Some(dir), None, &[], None, false);
assert!(
cmd.contains("--setting-sources user,project,local"),
"the relocated line must load the user tier: {cmd}"
);
let tiers = setting_source_tiers_of(setting_sources_flag(Some(dir)));
assert!(tiers.contains(&"user"), "{tiers:?}");
let assignment = cmd
.find("CLAUDE_CONFIG_DIR='/tm/claude-config'")
.expect("the relocated line must redirect the user tier");
let program = cmd.find(" claude ").expect("the line spawns claude");
assert!(
assignment < program,
"CLAUDE_CONFIG_DIR must be exported by env, not passed to claude: {cmd}"
);
}
fn assert_scrubbed_launch_line(cmd: &str) {
assert!(
cmd.starts_with("env -u "),
"the launch line must carry an env scrub prefix: {cmd}"
);
for marker in [
"CLAUDE_CODE_CHILD_SESSION",
"CLAUDE_CODE_SESSION_ID",
"CLAUDECODE",
"CLAUDE_PID",
"CLAUDE_EFFORT",
"CLAUDE_CODE_EXECPATH",
] {
assert!(
cmd.contains(&format!("-u {marker}")),
"the launch line must unset {marker}: {cmd}"
);
}
assert!(
!cmd.contains("CLAUDE_CONFIG_DIR"),
"this builder must not touch CLAUDE_CONFIG_DIR at all: {cmd}"
);
assert!(
!cmd.contains("-u CLAUDE_CODE_OAUTH_TOKEN"),
"must never unset the OAuth token (#2246): {cmd}"
);
}
#[test]
fn inplace_session_command_scrubs_inherited_session_markers() {
assert_scrubbed_launch_line(&build_inplace_session_command_configured(false));
assert_scrubbed_launch_line(&build_inplace_session_command_with_prompt(
Path::new("/tmp/p.txt"),
false,
));
}
#[test]
fn inplace_session_command_keeps_the_permission_flag_and_nothing_else() {
assert_eq!(
build_inplace_session_command_configured(false),
format!(
"env{} {} claude {PERMISSION_MODE_FLAG}",
crate::core::claude_env_scrub::env_unset_flags(),
crate::core::alt_screen::configured_shell_assignments(false)
)
);
assert!(
!build_inplace_session_command_configured(false).contains(SETTING_SOURCES_FLAG),
"must not add --setting-sources: that would drop the user settings tier"
);
}
#[test]
fn inplace_session_command_carries_the_prompt_file_and_the_permission_flag() {
let cmd = build_inplace_session_command_with_prompt(Path::new("/tmp/p.txt"), false);
assert_eq!(
cmd,
format!(
"env{} {} claude --append-system-prompt-file '/tmp/p.txt' {PERMISSION_MODE_FLAG}",
crate::core::claude_env_scrub::env_unset_flags(),
crate::core::alt_screen::configured_shell_assignments(false)
)
);
assert!(
!cmd.contains(SETTING_SOURCES_FLAG),
"must not add --setting-sources: that would drop the user settings tier"
);
}
#[test]
fn inplace_session_command_quotes_the_prompt_file() {
let cmd =
build_inplace_session_command_with_prompt(Path::new("/tmp/with space/p.txt"), false);
assert!(
cmd.contains("--append-system-prompt-file '/tmp/with space/p.txt' "),
"the prompt path must be one shell word: {cmd}"
);
}
#[test]
fn client_session_command_scrubs_inherited_session_markers() {
assert_scrubbed_launch_line(&build_client_session_command_configured(None, false));
assert_scrubbed_launch_line(&build_client_session_command_configured(
Some(Path::new("/tmp/p.txt")),
false,
));
}
#[test]
fn client_session_command_appends_the_prompt_file() {
let head = head();
assert_eq!(build_client_session_command_configured(None, false), head);
assert_eq!(
build_client_session_command_configured(Some(Path::new("/tmp/p.txt")), false),
format!("{head} --append-system-prompt-file /tmp/p.txt")
);
}
fn assert_assigns_the_configured_renderer(cmd: &str, alternate_screen: bool) {
let value = if alternate_screen { "0" } else { "1" };
let operand = format!("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN={value} ");
assert!(cmd.contains(&operand), "want {operand:?} in: {cmd}");
assert!(
!cmd.contains("${CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN"),
"the renderer must not yield to the pane environment: {cmd}"
);
let first_assignment = cmd.find('=').expect("the line carries an assignment");
let last_unset = cmd.rfind("-u ").expect("the line carries scrub flags");
assert!(
last_unset < first_assignment,
"the operand must follow every -u flag: {cmd}"
);
}
#[test]
fn claude_command_assigns_the_configured_renderer() {
for alternate_screen in [true, false] {
for cmd in [
build_claude_command_configured(None, None, None, &[], None, alternate_screen),
build_claude_command_with_configured(
None,
None,
Some(Path::new("/tm/claude-config")),
Some("tok"),
&[],
None,
alternate_screen,
),
] {
assert_assigns_the_configured_renderer(&cmd, alternate_screen);
}
}
}
#[test]
fn claude_command_defaults_the_mouse_capture_off() {
let operand = crate::core::alt_screen::MOUSE_SHELL_ASSIGNMENT;
for cmd in [
build_claude_command_configured(None, None, None, &[], None, false),
build_claude_command_configured(Some("claude-opus-4-5"), None, None, &[], None, false),
build_claude_command_with_configured(
None,
None,
Some(Path::new("/tm/claude-config")),
Some("tok"),
&[],
None,
false,
),
] {
assert!(
cmd.contains(operand),
"the launch line must default mouse capture off: {cmd}"
);
let first_assignment = cmd.find('=').expect("the line carries an assignment");
let last_unset = cmd.rfind("-u ").expect("the line carries scrub flags");
assert!(
last_unset < first_assignment,
"the operand must follow every -u flag: {cmd}"
);
}
}
#[test]
fn inplace_session_command_assigns_the_configured_renderer() {
for alternate_screen in [true, false] {
assert_assigns_the_configured_renderer(
&build_inplace_session_command_with_prompt(
Path::new("/tmp/p.txt"),
alternate_screen,
),
alternate_screen,
);
}
}
#[test]
fn inplace_session_command_defaults_the_mouse_capture_off() {
let cmd = build_inplace_session_command_with_prompt(Path::new("/tmp/p.txt"), false);
assert!(
cmd.contains(crate::core::alt_screen::MOUSE_SHELL_ASSIGNMENT),
"the in-place session line must default mouse capture off: {cmd}"
);
}
#[test]
fn client_session_command_assigns_the_configured_renderer() {
for alternate_screen in [true, false] {
for prompt in [None, Some(Path::new("/tmp/p.txt"))] {
assert_assigns_the_configured_renderer(
&build_client_session_command_configured(prompt, alternate_screen),
alternate_screen,
);
}
}
}
#[test]
fn client_session_command_defaults_the_mouse_capture_off() {
let operand = crate::core::alt_screen::MOUSE_SHELL_ASSIGNMENT;
assert!(build_client_session_command_configured(None, false).contains(operand));
assert!(
build_client_session_command_configured(Some(Path::new("/tmp/p.txt")), false)
.contains(operand),
"the prompt-file shape must carry it too"
);
}
#[test]
fn claude_command_with_prompt() {
let path = Path::new("/tmp/prompt.txt");
let cmd = build_claude_command_configured(None, Some(path), None, &[], None, false);
assert_eq!(
cmd,
format!(
"{} --append-system-prompt-file /tmp/prompt.txt {FLAGS}",
head()
)
);
}
#[test]
fn claude_command_with_both() {
let path = Path::new("/tmp/sys.txt");
let cmd = build_claude_command_configured(
Some("claude-haiku-4-5"),
Some(path),
None,
&[],
None,
false,
);
assert_eq!(
cmd,
format!(
"{} --model claude-haiku-4-5 --append-system-prompt-file /tmp/sys.txt {FLAGS}",
head()
)
);
}
#[test]
fn claude_command_carries_the_mcp_config_flag_and_not_strict() {
let scoped = Path::new("/state/session-mcp/ab12.json");
let cmd = build_claude_command_with_configured(
None,
None,
Some(Path::new("/tm/claude-config")),
None,
&[],
Some(scoped),
false,
);
assert!(
!cmd.contains("--strict-mcp-config"),
"the launch line must never narrow the user scope: {cmd}"
);
assert!(
cmd.contains("--mcp-config '/state/session-mcp/ab12.json'"),
"the pane shell re-splits this line, so the path must be quoted: {cmd}"
);
}
#[test]
fn claude_command_omits_the_mcp_config_flag_without_a_file() {
let cmd = build_claude_command_configured(None, None, None, &[], None, false);
assert!(!cmd.contains("--strict-mcp-config"), "{cmd}");
assert!(!cmd.contains("--mcp-config"), "{cmd}");
}
#[test]
fn claude_command_includes_setting_sources() {
let cmd = build_claude_command_configured(None, None, None, &[], None, false);
assert!(
cmd.contains("--setting-sources project,local"),
"missing setting-sources isolation flag: {cmd}"
);
assert!(
!cmd.contains("user"),
"should not reference the user source: {cmd}"
);
}
#[test]
fn claude_command_includes_permission_mode() {
let cmd = build_claude_command_configured(Some("sonnet"), None, None, &[], None, false);
assert!(
cmd.contains("--dangerously-skip-permissions"),
"missing bypass-permissions flag: {cmd}"
);
assert!(
!cmd.contains("acceptEdits"),
"old acceptEdits flag must not be present: {cmd}"
);
}
#[test]
fn write_prompt_file_in_returns_the_written_path() {
let tmp = tempfile::tempdir().unwrap();
let path = write_prompt_file_in(tmp.path(), "hello trusty-mpm").unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "hello trusty-mpm");
}
#[test]
fn write_pm_prompt_file_in_names_the_path_the_cause_and_the_project() {
let tmp = tempfile::tempdir().unwrap();
let not_a_dir = tmp.path().join("not-a-dir");
std::fs::write(¬_a_dir, "").unwrap();
let err = write_pm_prompt_file_in(¬_a_dir, "p", Path::new("/proj"), "launch")
.unwrap_err()
.to_string();
assert!(err.contains(&*not_a_dir.to_string_lossy()), "{err}");
assert!(err.contains("os error"), "{err}");
assert!(
err.contains("refusing to launch /proj without its PM"),
"{err}"
);
}
#[test]
fn write_prompt_file_in_names_the_path_and_the_cause() {
let tmp = tempfile::tempdir().unwrap();
let not_a_dir = tmp.path().join("not-a-dir");
std::fs::write(¬_a_dir, "").unwrap();
let err = write_prompt_file_in(¬_a_dir, "prompt").unwrap_err();
assert!(err.path.starts_with(¬_a_dir), "{}", err.path.display());
let text = err.to_string();
assert!(text.contains(&*err.path.to_string_lossy()), "{text}");
assert!(text.contains(&err.cause.to_string()), "{text}");
}
#[cfg(unix)]
#[test]
fn write_prompt_file_in_creates_an_owner_only_file() {
use std::os::unix::fs::PermissionsExt as _;
let tmp = tempfile::tempdir().unwrap();
let path = write_prompt_file_in(tmp.path(), "secret prompt").unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "the prompt file must be owner-only: {mode:o}");
}
#[test]
fn write_new_private_refuses_an_existing_file() {
let tmp = tempfile::tempdir().unwrap();
let planted = tmp.path().join("planted.txt");
std::fs::write(&planted, "planted").unwrap();
let err = write_new_private(&planted, "prompt").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
assert_eq!(std::fs::read_to_string(&planted).unwrap(), "planted");
}
#[test]
fn pm_model_resolution() {
let cfg = MpmConfig::default();
let m = resolve_pm_model(&cfg, None);
assert_eq!(m, "claude-sonnet-4-5");
let m = resolve_pm_model(&cfg, Some("haiku"));
assert_eq!(m, "claude-haiku-4-5");
}
#[test]
fn agent_command_uses_config_model() {
let dir = tempfile::TempDir::new().unwrap();
let toml = "[models.agents]\nengineer = \"haiku\"\n";
std::fs::write(dir.path().join("config.toml"), toml).unwrap();
let cfg = MpmConfig::load(dir.path());
let agent = AgentSummary {
name: "engineer".to_string(),
role: "engineer".to_string(),
description: None,
model: Some("sonnet".to_string()),
extends_chain: vec![],
};
let cmd = build_agent_command_configured(&cfg, &agent, None, None, false);
assert_eq!(cmd, format!("{} --model claude-haiku-4-5 {FLAGS}", head()));
}
#[test]
fn setting_source_tiers_parses_the_flag() {
assert_eq!(setting_source_tiers(), vec!["project", "local"]);
assert!(
!setting_source_tiers().contains(&"user"),
"on a spawn that does NOT relocate CLAUDE_CONFIG_DIR the `user` tier \
is the operator's real ~/.claude and stays excluded (issue #1269)"
);
}
#[test]
fn relocated_setting_sources_flag_loads_the_user_tier() {
assert!(
relocated_setting_source_tiers().contains(&"user"),
"the relocated spawn must load the `user` tier — that is where the \
bundled roster lives once CLAUDE_CONFIG_DIR is redirected"
);
assert_eq!(
relocated_setting_source_tiers(),
vec!["user", "project", "local"]
);
}
#[test]
fn setting_sources_flag_picks_by_config_dir() {
assert_eq!(
setting_sources_flag(Some(Path::new("/tm/claude-config"))),
SETTING_SOURCES_FLAG_RELOCATED
);
assert_eq!(setting_sources_flag(None), SETTING_SOURCES_FLAG);
}
#[test]
fn settings_tier_of_names_project_for_the_harness_cwd() {
let cwd = Path::new("/work/checkout");
assert_eq!(settings_tier_of(cwd, cwd), "project");
}
#[test]
fn settings_tier_of_names_user_for_anything_else() {
assert_eq!(
settings_tier_of(Path::new("/Users/someone"), Path::new("/work/checkout")),
"user"
);
}
}