use super::entry::isolated_framework_paths;
use super::search_index::register_project_index;
use super::settings::{
clean_global_trusty_memory_hooks, deploy_output_style, is_stale_bare_statusline_command,
is_stale_statusline_command, preseed_workspace_trust, resolve_palace_slug,
resolve_statusline_binary_with, write_auto_memory_enabled, write_enabled_plugins,
write_output_style, write_project_hooks, write_status_line,
};
use super::*;
use crate::core::content_source::test_support::rc;
use tempfile::tempdir;
pub(super) struct EnvVarGuard {
key: &'static str,
prev: Option<String>,
}
impl EnvVarGuard {
pub(super) fn set(key: &'static str, value: &std::path::Path) -> Self {
let prev = std::env::var(key).ok();
unsafe {
std::env::set_var(key, value);
}
Self { key, prev }
}
pub(super) fn set_str(key: &'static str, value: &str) -> Self {
let prev = std::env::var(key).ok();
unsafe {
std::env::set_var(key, value);
}
Self { key, prev }
}
pub(super) fn clear(key: &'static str) -> Self {
let prev = std::env::var(key).ok();
unsafe {
std::env::remove_var(key);
}
Self { key, prev }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe {
match self.prev.take() {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
#[test]
fn build_system_prompt_includes_trusty_block() {
let prompt = build_system_prompt(rc()).expect("trusty block is always present");
assert!(prompt.contains("## Trusty Tool Priority (Non-Overridable)"));
assert!(prompt.contains("mcp__trusty-memory__memory_recall"));
assert!(prompt.contains("mcp__trusty-search__search"));
assert!(prompt.contains("# PM Agent -- Trusty MPM"));
}
fn pin_prompt_feedback_off(project_dir: &std::path::Path) {
std::fs::write(
project_dir.join(crate::core::project_config::PROJECT_CONFIG_FILE),
"prompt_self_improvement = false\n",
)
.unwrap();
}
#[test]
fn build_system_prompt_for_applies_project_override() {
let tmp = tempdir().unwrap();
let project = tmp.path();
pin_prompt_feedback_off(project);
std::fs::write(
project.join("CLAUDE.md"),
"<!-- TRUSTY-MPM: WORKFLOW START v=1 -->\n\
PROJECT_OVERRIDE_MARKER\n\
<!-- TRUSTY-MPM: WORKFLOW END -->\n",
)
.unwrap();
let override_dir = project.join(".trusty-mpm");
std::fs::create_dir_all(&override_dir).unwrap();
std::fs::write(override_dir.join("INSTRUCTIONS.md"), "RETIRED_MARKER\n").unwrap();
let prompt = build_system_prompt_for(rc(), project);
assert!(prompt.contains("PROJECT_OVERRIDE_MARKER"));
assert!(
!prompt.contains("RETIRED_MARKER"),
"the retired .trusty-mpm/INSTRUCTIONS.md must not reach the launch prompt"
);
assert!(prompt.contains("## Prohibitions (CANONICAL"));
assert!(prompt.contains("# PM Agent -- Trusty MPM"));
}
#[test]
fn build_system_prompt_for_no_override_matches_bundled_sections() {
let tmp = tempdir().unwrap();
pin_prompt_feedback_off(tmp.path());
let prompt = build_system_prompt_for(rc(), tmp.path());
assert!(prompt.contains("# PM Agent -- Trusty MPM"));
assert!(prompt.contains("# Agent Delegation Routing"));
let base = prompt.find("## Prohibitions (CANONICAL").expect("base");
let deleg = prompt.find("# Agent Delegation Routing").expect("deleg");
assert!(base > deleg, "BASE_PM floor must be last");
}
#[test]
fn prepare_session_does_not_seed_the_workspace_on_the_managed_path() {
let managed_root = tempdir().unwrap();
let workspace = tempdir().unwrap();
let home = tempdir().unwrap();
let fw = crate::core::paths::FrameworkPaths::for_managed_project(
managed_root.path(),
workspace.path(),
);
assert_eq!(
fw.claude_agents_dir(),
workspace.path().join(".claude").join("agents"),
"fixture must genuinely be the managed shape, else this test is vacuous"
);
prepare_session_with_home(&fw, workspace.path(), None, true, Some(home.path()))
.expect("prep succeeds");
assert!(
!workspace.path().join(".claude.json").exists(),
"the managed layout must not capture the user-global trust seed (#5544)"
);
let seeded = home.path().join(".claude.json");
assert!(
seeded.is_file(),
"the seed must land on the supplied home; expected {}",
seeded.display()
);
assert!(
std::fs::read_to_string(&seeded)
.expect("seed readable")
.contains(&workspace.path().to_string_lossy().to_string()),
"the seed must name the prepared workspace"
);
}
#[test]
fn global_hook_cleanup_reaches_the_real_home_under_an_overridden_root() {
let home = tempdir().unwrap();
let workspace = tempdir().unwrap();
let settings = home.path().join(".claude").join("settings.json");
std::fs::create_dir_all(settings.parent().unwrap()).unwrap();
std::fs::write(
&settings,
r#"{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"trusty-memory hooks fire UserPromptSubmit"}]}]}}"#,
)
.unwrap();
let fw = crate::core::paths::FrameworkPaths::for_managed_project(
std::path::Path::new("/opt/tm-roots/team-a/.trusty-mpm"),
workspace.path(),
);
prepare_session_with_home(&fw, workspace.path(), None, true, Some(home.path()))
.expect("prep succeeds");
let after = std::fs::read_to_string(&settings).expect("settings still readable");
assert!(
!after.contains("trusty-memory hooks fire"),
"the global trusty-memory hook entry must actually be removed — a cleanup \
pointed at a nonexistent file returns Ok(()) and looks identical (#5544). \
Settings after: {after}"
);
assert!(
!workspace
.path()
.join(".claude")
.join("settings.json")
.exists()
|| !std::fs::read_to_string(workspace.path().join(".claude").join("settings.json"))
.unwrap_or_default()
.contains("trusty-memory hooks fire"),
"and the overridden install root must not have redirected it into the workspace"
);
}
#[test]
fn home_writes_are_skipped_when_the_home_is_unusable() {
let workspace = tempdir().unwrap();
for home in [None, Some(std::path::Path::new("."))] {
super::settings::preseed_workspace_trust_home(home, workspace.path()).expect("soft skip");
super::settings::remove_global_trusty_memory_hooks(home).expect("soft skip");
}
assert!(
!std::path::Path::new("./.claude.json").exists(),
"an absent or relative home must decline, never write into the working directory"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_stash_reflects_override() {
for native_supported in [true, false] {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::write(
project.join("CLAUDE.md"),
"<!-- TRUSTY-MPM: WORKFLOW START v=1 -->\n\
# Custom Workflow\n\nSTASH_OVERRIDE_MARKER\n\
<!-- TRUSTY-MPM: WORKFLOW END -->\n",
)
.unwrap();
let report = prepare_session_with_style_and_native(&fw, project, None, native_supported)
.expect("prep succeeds");
let stash = std::fs::read_to_string(&report.stash).expect("stash readable");
assert!(
stash.contains("STASH_OVERRIDE_MARKER"),
"stash must reflect the CLAUDE.md WORKFLOW override (native_supported={native_supported})"
);
assert!(
!stash.contains("# PM Workflow Configuration"),
"bundled workflow heading must be replaced in the stash (native_supported={native_supported})"
);
assert!(
stash.contains("## Prohibitions (CANONICAL"),
"stash must still carry the BASE_PM floor (native_supported={native_supported})"
);
assert_eq!(
stash,
build_system_prompt_for_with_style_and_native(rc(), project, None, native_supported),
"stash must equal the launch prompt (native_supported={native_supported})"
);
if native_supported {
assert!(
!stash.contains(crate::core::output_style::INJECTED_STYLE_HEADING),
"native-capable: stash must NOT carry the injected style block"
);
} else {
assert!(
stash.contains(crate::core::output_style::INJECTED_STYLE_HEADING),
"native-incapable: stash MUST carry the injected style block"
);
}
}
}
#[test]
#[serial_test::serial]
fn prepare_session_writes_claude_md_and_stash() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let report = prepare_session(&fw, project).expect("prep succeeds");
assert!(
project.join("CLAUDE.md").exists(),
"CLAUDE.md must exist after prep"
);
assert!(
report.stash.exists(),
"merged instructions stash must be written"
);
assert_eq!(
report.stash,
project.join(".trusty-mpm").join("last-instructions.md")
);
}
#[test]
fn instruction_failure_is_fatal() {
let err = PrepError::Instructions {
path: PathBuf::from("/p/.trusty-mpm/framework/INSTRUCTIONS-COMPILED.md"),
source: std::io::Error::other("boom"),
};
assert!(err.is_fatal());
let shown = err.to_string();
assert!(shown.contains("/p/.trusty-mpm/framework/INSTRUCTIONS-COMPILED.md"));
assert!(shown.contains("was NOT started"));
}
#[test]
fn content_failure_is_fatal() {
let cache = tempfile::tempdir().expect("tempdir");
let source = crate::core::content_source::framework_content_in(
cache.path(),
crate::core::content_source::DevOverride::Off,
)
.expect_err("an empty cache serves nothing");
let err = PrepError::Content { source };
assert!(err.is_fatal());
assert!(err.to_string().contains("tm content install"), "{err}");
}
#[test]
fn deploy_and_io_failures_stay_non_fatal() {
assert!(!PrepError::Deploy("agents".into()).is_fatal());
assert!(!PrepError::SkillDeploy("skills".into()).is_fatal());
assert!(
!PrepError::Io {
path: PathBuf::from("/p/.trusty-mpm/last-instructions.md"),
source: std::io::Error::other("boom"),
}
.is_fatal()
);
}
#[test]
#[serial_test::serial]
fn prepare_session_writes_the_compiled_prompt_before_returning() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
const STALE: &str = "STALE-FROM-A-PREVIOUS-INSTALL";
let compiled = compiled_for(project);
std::fs::create_dir_all(compiled.parent().unwrap()).unwrap();
std::fs::write(&compiled, STALE).unwrap();
let report = prepare_session(&fw, project).expect("prep succeeds");
assert_eq!(
report.compiled_prompt, compiled,
"the report must name the compiled path the launch actually wrote"
);
let on_disk = std::fs::read_to_string(&compiled).expect("compiled prompt must be readable");
assert_ne!(
on_disk, STALE,
"the launch must overwrite a stale compiled prompt, not leave it in place"
);
let launch_prompt = std::fs::read_to_string(&report.stash).expect("stash must be readable");
assert_eq!(
on_disk, launch_prompt,
"the compiled prompt must be byte-identical to the text passed to claude"
);
assert!(!on_disk.is_empty(), "the compiled prompt must not be empty");
}
#[test]
#[serial_test::serial]
fn prepare_session_fails_when_the_compiled_prompt_cannot_be_written() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(compiled_for(project)).unwrap();
let err = prepare_session(&fw, project)
.expect_err("a failed compiled-prompt write must abort the launch preparation");
assert!(
err.is_fatal(),
"a compiled-prompt write failure must be classified fatal, got {err:?}"
);
match &err {
PrepError::Instructions { path, .. } => assert_eq!(
*path,
compiled_for(project),
"the error must name the compiled prompt path"
),
other => panic!("expected PrepError::Instructions, got {other:?}"),
}
let shown = err.to_string();
assert!(
shown.contains("was NOT started"),
"must tell the operator the session did not start: {shown}"
);
assert!(
shown.contains(&compiled_display(project)),
"must name the path: {shown}"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_for_managed_writes_the_per_session_compiled_prompt() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let report =
prepare_session_for_managed(&fw, project, None, "sess-abc").expect("prep succeeds");
let expected = crate::core::instruction_pipeline::compiled_prompt_path(project, "sess-abc");
assert_eq!(
report.compiled_prompt, expected,
"the report must name this session's compiled path"
);
assert!(expected.exists(), "the compiled prompt must be on disk");
assert!(
expected
.to_string_lossy()
.contains("/.trusty-mpm/sessions/sess-abc/"),
"the layout is `.trusty-mpm/sessions/<id>/`: {}",
expected.display()
);
assert_ne!(
expected,
crate::core::instruction_pipeline::compiled_prompt_path(project, "sess-other"),
"a second session must not share this file"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_migrates_off_the_legacy_compiled_prompt() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let legacy = project
.join(".trusty-mpm")
.join("framework")
.join("INSTRUCTIONS-COMPILED.md");
std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
std::fs::write(&legacy, "OLD COMPILED PROMPT").unwrap();
prepare_session(&fw, project).expect("prep succeeds");
assert!(
!legacy.exists(),
"the pre-#4832 per-project compiled prompt must be retired: {}",
legacy.display()
);
}
fn compiled_for(project: &std::path::Path) -> std::path::PathBuf {
let scope = crate::core::harness_root::session_scope(None);
crate::core::instruction_pipeline::compiled_prompt_path(project, &scope)
}
fn compiled_display(project: &std::path::Path) -> String {
compiled_for(project).display().to_string()
}
#[test]
#[serial_test::serial]
fn prepare_session_refuses_when_the_instructions_cannot_be_built() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(project.join("CLAUDE.md"))
.expect("plant a directory where CLAUDE.md goes");
let err = prepare_session(&fw, project)
.expect_err("a session whose instructions cannot be built must NOT start");
assert!(
err.is_fatal(),
"an instruction-build failure must be classified fatal, got {err:?}"
);
match &err {
PrepError::Instructions { path, .. } => assert!(
path.starts_with(project),
"the error must name the offending project path, got {}",
path.display()
),
other => panic!("expected PrepError::Instructions, got {other:?}"),
}
let shown = err.to_string();
assert!(
shown.contains("was NOT started"),
"must say the session did not start: {shown}"
);
}
#[test]
#[serial_test::serial]
fn stash_write_failure_does_not_skip_the_fatal_instruction_write() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(project.join(".trusty-mpm/last-instructions.md"))
.expect("plant a directory where the stash file goes");
let report = prepare_session(&fw, project)
.expect("a failed stash write must NOT refuse the launch (#2149) nor abort preparation");
assert!(
report.compiled_prompt.exists(),
"the compiled prompt must still be written when only the stash fails"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_deploys_project_tier_output_style() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
prepare_session(&fw, project).expect("prep succeeds");
let project_style = project.join(".claude/output-styles/trusty-mpm.md");
assert!(
project_style.exists(),
"project-tier output style file must be deployed: {}",
project_style.display()
);
}
#[test]
#[serial_test::serial]
fn prepare_session_self_heals_missing_skill_source() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
assert!(!fw.skills.exists(), "precondition: no prior tm install ran");
let report = prepare_session(&fw, project).expect("prep succeeds");
assert!(
!report.skill_deploy.deployed.is_empty(),
"session prep must self-heal the missing skill source and deploy at \
least one skill; got {:?}",
report.skill_deploy
);
assert!(fw.skills.join("tm-doctor.md").exists());
assert!(
fw.skill_deploy_dir()
.join("tm-doctor")
.join("SKILL.md")
.is_file(),
"the healed skill must be deployed to the managed tier: {}",
fw.skill_deploy_dir().display()
);
assert!(
!fw.claude_skills_dir().join("tm-doctor").exists(),
"a bundled skill must never be deployed to the project tier"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_self_heals_renamed_skill_source() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(&fw.skills).unwrap();
std::fs::write(fw.skills.join("mpm-old-skill.md"), "stale\n").unwrap();
let report = prepare_session(&fw, project).expect("prep succeeds");
assert!(
!fw.skills.join("mpm-old-skill.md").exists(),
"the stale pre-rename file must be pruned during self-heal"
);
assert!(
!report.skill_deploy.deployed.is_empty(),
"renamed/stale skill source must self-heal and deploy current skills"
);
assert!(
fw.skill_deploy_dir()
.join("tm-doctor")
.join("SKILL.md")
.is_file(),
"the refreshed roster must be deployed to the managed tier: {}",
fw.skill_deploy_dir().display()
);
assert!(
!fw.claude_skills_dir().join("tm-doctor").exists(),
"a bundled skill must never be deployed to the project tier"
);
}
#[tokio::test]
#[serial_test::serial]
async fn prepare_session_emits_stage_events_in_order() {
use crate::core::provisioning_stage::{ProvisioningStage, StageEmitter, scoped};
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let (tx, mut rx) = tokio::sync::broadcast::channel(32);
let emitter = StageEmitter::new("test-session", "https://github.com/acme/widgets", tx);
scoped(emitter, async {
prepare_session(&fw, project).expect("prep succeeds");
})
.await;
let mut stages = Vec::new();
while let Ok(value) = rx.try_recv() {
assert_eq!(value["kind"], "provisioning_stage");
assert_eq!(value["repo_url"], "https://github.com/acme/widgets");
stages.push(value["stage"].as_str().unwrap().to_string());
}
assert_eq!(
stages,
vec![
ProvisioningStage::DeployingAgents.wire_name(),
ProvisioningStage::DeployingSkills.wire_name(),
ProvisioningStage::BuildingInstructions.wire_name(),
ProvisioningStage::ConfiguringMcp.wire_name(),
],
"prepare_session must emit exactly these four stages, in order"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_sets_output_style() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
prepare_session(&fw, project).expect("prep succeeds");
let settings_path = project.join(".claude").join("settings.json");
assert!(settings_path.exists(), ".claude/settings.json must exist");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert_eq!(value["outputStyle"], serde_json::json!("trusty-mpm"));
}
#[test]
#[serial_test::serial]
fn prepare_session_disables_auto_memory_when_trusty_memory_is_reachable() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
crate::core::session_launch::prepare_session_with_memory_reachable(
&fw,
project,
Some(tmp_home.path()),
true,
)
.expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["autoMemoryEnabled"],
serde_json::json!(false),
"trusty-memory is up and is the memory — Claude Code auto-memory must be off"
);
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm"),
"the auto-memory write must not clobber the style the same launch wrote"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_restores_auto_memory_when_trusty_memory_is_down() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(project.join(".claude")).unwrap();
std::fs::write(
project.join(".claude").join("settings.json"),
r#"{"autoMemoryEnabled":false}"#,
)
.unwrap();
let report = crate::core::session_launch::prepare_session_with_memory_reachable(
&fw,
project,
Some(tmp_home.path()),
false,
)
.expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["autoMemoryEnabled"],
serde_json::json!(true),
"with trusty-memory down the fallback must be turned back ON: {value}"
);
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm"),
"the auto-memory write must not skip the rest of the launch"
);
assert!(
!report.memory_reachable,
"the report must carry the reachability the adapter will reuse"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_reports_the_resolved_reachability() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let report = crate::core::session_launch::prepare_session_with_memory_reachable(
&fw,
tmp.path(),
Some(tmp_home.path()),
true,
)
.expect("prep succeeds");
assert!(report.memory_reachable);
}
#[test]
#[serial_test::serial]
fn prepare_session_writes_configured_style() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(&fw.root).unwrap();
std::fs::write(
fw.config_toml(),
"[style]\nactive = \"trusty-mpm-teacher\"\n",
)
.unwrap();
prepare_session(&fw, project).expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm-teacher")
);
}
#[test]
#[serial_test::serial]
fn prepare_session_explicit_style_overrides_config() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(&fw.root).unwrap();
std::fs::write(
fw.config_toml(),
"[style]\nactive = \"trusty-mpm-teacher\"\n",
)
.unwrap();
crate::core::session_launch::prepare_session_with_style(
&fw,
project,
Some("trusty-mpm-research"),
)
.expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm-research")
);
}
#[test]
#[serial_test::serial]
fn prepare_session_unknown_style_falls_back_to_default() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
std::fs::create_dir_all(&fw.root).unwrap();
std::fs::write(fw.config_toml(), "[style]\nactive = \"does-not-exist\"\n").unwrap();
prepare_session(&fw, project).expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(value["outputStyle"], serde_json::json!("trusty-mpm"));
}
#[test]
fn write_output_style_preserves_existing_keys() {
let tmp = tempdir().unwrap();
let project = tmp.path();
let claude_dir = project.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"theme":"dark","outputStyle":"old"}"#,
)
.unwrap();
write_output_style(project, None).expect("write succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(claude_dir.join("settings.json")).unwrap())
.unwrap();
assert_eq!(value["outputStyle"], serde_json::json!("trusty-mpm"));
assert_eq!(value["theme"], serde_json::json!("dark"));
}
#[test]
fn write_auto_memory_off_disables_auto_memory() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_auto_memory_enabled(project, false).expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(value["autoMemoryEnabled"], serde_json::json!(false));
}
#[test]
fn write_auto_memory_off_preserves_existing_keys() {
let tmp = tempdir().unwrap();
let project = tmp.path();
let claude_dir = project.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"theme":"dark","outputStyle":"trusty-mpm"}"#,
)
.unwrap();
write_auto_memory_enabled(project, false).expect("write succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(claude_dir.join("settings.json")).unwrap())
.unwrap();
assert_eq!(value["autoMemoryEnabled"], serde_json::json!(false));
assert_eq!(value["theme"], serde_json::json!("dark"));
assert_eq!(value["outputStyle"], serde_json::json!("trusty-mpm"));
}
#[test]
fn write_auto_memory_off_overrides_an_enabled_value() {
let tmp = tempdir().unwrap();
let project = tmp.path();
let claude_dir = project.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"autoMemoryEnabled":true}"#,
)
.unwrap();
write_auto_memory_enabled(project, false).expect("write succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(claude_dir.join("settings.json")).unwrap())
.unwrap();
assert_eq!(value["autoMemoryEnabled"], serde_json::json!(false));
}
#[test]
fn write_auto_memory_on_reverts_a_prior_disable() {
let tmp = tempdir().unwrap();
let project = tmp.path();
let claude_dir = project.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"autoMemoryEnabled":false,"theme":"dark"}"#,
)
.unwrap();
write_auto_memory_enabled(project, true).expect("write succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(claude_dir.join("settings.json")).unwrap())
.unwrap();
assert_eq!(value["autoMemoryEnabled"], serde_json::json!(true));
assert_eq!(value["theme"], serde_json::json!("dark"));
}
#[test]
fn write_output_style_seeds_attribution() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_output_style(project, None).expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["attribution"]["commit"].as_str(),
Some(crate::core::attribution::ATTRIBUTION_FOOTER),
"attribution.commit must be the shared trusty-mpm footer"
);
assert_eq!(
value["attribution"]["pr"].as_str(),
Some(crate::core::attribution::ATTRIBUTION_FOOTER),
"attribution.pr must be the shared trusty-mpm footer"
);
}
#[test]
fn write_output_style_preserves_operator_attribution() {
let tmp = tempdir().unwrap();
let project = tmp.path();
let claude_dir = project.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"attribution":{"commit":"operator text","pr":""}}"#,
)
.unwrap();
write_output_style(project, None).expect("write succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(claude_dir.join("settings.json")).unwrap())
.unwrap();
assert_eq!(
value["attribution"]["commit"].as_str(),
Some("operator text"),
"an operator-set attribution.commit must survive the seed"
);
assert_eq!(
value["attribution"]["pr"].as_str(),
Some(""),
"an operator-set attribution.pr must survive the seed"
);
}
#[test]
fn write_output_style_sets_active_style() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_output_style(project, Some("trusty-mpm-research")).expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm-research")
);
}
#[test]
fn write_output_style_empty_falls_back_to_default() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_output_style(project, Some(" ")).expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(value["outputStyle"], serde_json::json!("trusty-mpm"));
}
#[test]
fn write_output_style_sets_spinner_tips() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_output_style(project, None).expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(value["spinnerTipsEnabled"], serde_json::json!(true));
let tips = value["spinnerTipsOverride"]["tips"]
.as_array()
.expect("spinnerTipsOverride.tips must be an array");
assert!(!tips.is_empty(), "spinner tips must be non-empty");
assert!(tips.iter().all(|tip| tip.is_string()));
}
#[test]
fn write_project_hooks_writes_all_event_types() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
let hooks = value["hooks"].as_object().expect("hooks must be an object");
for event in ["UserPromptSubmit", "SessionStart"] {
let groups = hooks[event]
.as_array()
.unwrap_or_else(|| panic!("{event} must be an array"));
assert!(!groups.is_empty(), "{event} must have a handler group");
let cmd = groups[0]["hooks"][0]["command"]
.as_str()
.expect("command must be a string");
assert!(
cmd.starts_with("trusty-memory "),
"{event} command must invoke trusty-memory: {cmd}"
);
}
}
#[test]
fn write_project_hooks_uses_canonical_commands() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
let raw = serde_json::to_string(&value).unwrap();
assert!(
!raw.contains("hooks fire"),
"the broken `hooks fire` command must never be written: {raw}"
);
assert_eq!(
value["hooks"]["UserPromptSubmit"][0]["hooks"][0]["command"],
serde_json::json!("trusty-memory prompt-context")
);
assert_eq!(
value["hooks"]["SessionStart"][0]["hooks"][0]["command"],
serde_json::json!("trusty-memory inbox-check")
);
}
#[test]
fn write_project_hooks_omits_post_tool_use_and_stop() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
for event in ["PostToolUse", "Stop"] {
let groups = value["hooks"][event]
.as_array()
.unwrap_or_else(|| panic!("{event} must be registered by the lifecycle triad"));
for group in groups {
let cmd = group["hooks"][0]["command"].as_str().unwrap();
assert!(
!cmd.starts_with("trusty-memory"),
"{event} must never carry a trusty-memory command: {cmd}"
);
}
}
}
#[test]
fn write_project_hooks_registers_pm_guard() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
let groups = value["hooks"]["PreToolUse"]
.as_array()
.expect("PreToolUse must be an array");
assert_eq!(
groups.len(),
2,
"PreToolUse must carry the PM-guard group plus the lifecycle-triad group"
);
let guard = groups
.iter()
.find(|g| g["matcher"] == serde_json::json!(""))
.expect("the guard group (matcher \"\") must be present");
let cmd = guard["hooks"][0]["command"]
.as_str()
.expect("command must be a string");
assert!(
cmd.ends_with(" hook --pm-guard"),
"guard command must end with ' hook --pm-guard', got: {cmd}"
);
assert!(
!cmd.starts_with("trusty-memory"),
"the guard group must be the tm guard, not a trusty-memory hook: {cmd}"
);
assert_eq!(
guard["hooks"][0]["timeout"],
serde_json::json!(10),
"the guard's registered timeout must stay above its read + audit budget"
);
}
#[test]
fn write_project_hooks_replaces_existing() {
let tmp = tempdir().unwrap();
let project = tmp.path();
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("first write succeeds");
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("second write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
let ss = value["hooks"]["SessionStart"]
.as_array()
.expect("SessionStart must be an array");
assert_eq!(
ss.len(),
2,
"re-running must not duplicate our own handler groups"
);
write_project_hooks(
project,
Some(std::path::Path::new("/usr/local/bin/tm")),
true,
false,
)
.expect("third write succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["hooks"]["UserPromptSubmit"].as_array().unwrap().len(),
1
);
}
fn init_git_repo_with_origin(dir: &std::path::Path, remote_url: &str) -> bool {
let init = std::process::Command::new("git")
.arg("init")
.arg("-q")
.current_dir(dir)
.status();
match init {
Ok(status) if status.success() => {}
_ => return false,
}
let remote = std::process::Command::new("git")
.args(["remote", "add", "origin", remote_url])
.current_dir(dir)
.status();
matches!(remote, Ok(status) if status.success())
}
#[test]
fn remove_global_hooks_removes_trusty_memory_entries() {
let tmp = tempdir().unwrap();
let settings_path = tmp.path().join("settings.json");
std::fs::write(
&settings_path,
r#"{
"theme": "dark",
"hooks": {
"PostToolUse": [
{ "matcher": "*", "hooks": [ { "type": "command", "command": "bash track.sh" } ] },
{ "matcher": "Write|Edit|Bash", "hooks": [ { "type": "command", "command": "trusty-memory hooks fire claude.post-tool-use" } ] }
],
"Stop": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "trusty-memory hooks fire claude.stop" } ] }
],
"UserPromptSubmit": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "trusty-memory hooks fire claude.user-prompt" } ] }
]
}
}"#,
)
.unwrap();
clean_global_trusty_memory_hooks(&settings_path).expect("clean succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert_eq!(value["theme"], serde_json::json!("dark"));
let post = value["hooks"]["PostToolUse"].as_array().unwrap();
assert_eq!(post.len(), 1);
assert!(
post[0]["hooks"][0]["command"]
.as_str()
.unwrap()
.contains("track.sh")
);
assert!(
value["hooks"].get("Stop").is_none(),
"empty Stop event must be removed"
);
assert!(
value["hooks"].get("UserPromptSubmit").is_none(),
"empty UserPromptSubmit event must be removed"
);
}
#[test]
fn remove_global_hooks_tolerates_missing_file() {
let tmp = tempdir().unwrap();
let missing = tmp.path().join("nope.json");
clean_global_trusty_memory_hooks(&missing).expect("missing file is a no-op");
}
#[test]
#[serial_test::serial]
fn register_project_index_withholds_id_when_registration_is_unconfirmed() {
let data_dir = tempdir().unwrap();
let _env = EnvVarGuard::set(
trusty_common::data_dir::DATA_DIR_OVERRIDE_ENV,
data_dir.path(),
);
let project = tempdir().unwrap();
std::fs::create_dir_all(project.path().join(".git")).unwrap();
let nested = project.path().join("crates/inner");
std::fs::create_dir_all(&nested).unwrap();
let report = trusty_common::search_index::ensure_project_indexed_reporting(
&nested,
trusty_common::search_index::IndexOptions::default(),
);
let expected = trusty_common::derive_index_id(project.path());
assert_eq!(
report.index_id,
Some(expected),
"id is the git-root basename"
);
assert_eq!(
register_project_index(&nested),
None,
"no registration was confirmed, so the stub must not be pinned (#5091)"
);
}
#[test]
fn preseed_trust_marks_directory() {
let tmp = tempdir().unwrap();
let claude_json = tmp.path().join(".claude.json");
let workspace = tmp.path().join("ws");
preseed_workspace_trust(&claude_json, &workspace).expect("seed succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&claude_json).unwrap()).unwrap();
let key = workspace.to_string_lossy().to_string();
let entry = &value["projects"][&key];
assert_eq!(entry["hasTrustDialogAccepted"], serde_json::json!(true));
assert_eq!(
entry["hasCompletedProjectOnboarding"],
serde_json::json!(true)
);
assert!(
entry["projectOnboardingSeenCount"].as_u64().unwrap() >= 1,
"onboarding counter must be >= 1"
);
}
#[test]
fn preseed_trust_preserves_other_keys() {
let tmp = tempdir().unwrap();
let claude_json = tmp.path().join(".claude.json");
let workspace = tmp.path().join("ws");
std::fs::write(
&claude_json,
r#"{"oauthAccount":{"emailAddress":"r@1mc.io"},"projects":{"/other":{"hasTrustDialogAccepted":true}}}"#,
)
.unwrap();
preseed_workspace_trust(&claude_json, &workspace).expect("seed succeeds");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&claude_json).unwrap()).unwrap();
assert_eq!(
value["oauthAccount"]["emailAddress"],
serde_json::json!("r@1mc.io")
);
assert_eq!(
value["projects"]["/other"]["hasTrustDialogAccepted"],
serde_json::json!(true)
);
let key = workspace.to_string_lossy().to_string();
assert_eq!(
value["projects"][&key]["hasTrustDialogAccepted"],
serde_json::json!(true)
);
}
#[test]
fn preseed_trust_is_idempotent() {
let tmp = tempdir().unwrap();
let claude_json = tmp.path().join(".claude.json");
let workspace = tmp.path().join("ws");
preseed_workspace_trust(&claude_json, &workspace).expect("first seed");
let first = std::fs::read_to_string(&claude_json).unwrap();
preseed_workspace_trust(&claude_json, &workspace).expect("second seed");
let second = std::fs::read_to_string(&claude_json).unwrap();
assert_eq!(first, second, "re-seeding must leave the file unchanged");
}
#[test]
fn preseed_trust_leaves_malformed_file() {
let tmp = tempdir().unwrap();
let claude_json = tmp.path().join(".claude.json");
let workspace = tmp.path().join("ws");
let garbage = "{ this is not valid json ";
std::fs::write(&claude_json, garbage).unwrap();
preseed_workspace_trust(&claude_json, &workspace).expect("soft-fails to Ok");
let after = std::fs::read_to_string(&claude_json).unwrap();
assert_eq!(after, garbage, "malformed file must be left untouched");
}
#[test]
fn deploy_output_style_writes_file() {
let home = tempdir().unwrap();
let path = deploy_output_style(rc(), home.path()).expect("deploy succeeds");
assert_eq!(
path,
home.path()
.join(".claude")
.join("output-styles")
.join("trusty-mpm.md")
);
let written = std::fs::read_to_string(&path).expect("style file readable");
assert_eq!(written, rc().required("output-styles/trusty-mpm.md"));
assert!(written.contains("name: trusty-mpm"));
}
#[test]
fn deploy_output_style_overwrites() {
let home = tempdir().unwrap();
let first = deploy_output_style(rc(), home.path()).expect("first deploy succeeds");
std::fs::write(&first, "stale operator content").unwrap();
let second = deploy_output_style(rc(), home.path()).expect("second deploy succeeds");
assert_eq!(first, second);
let written = std::fs::read_to_string(&second).unwrap();
assert_eq!(written, rc().required("output-styles/trusty-mpm.md"));
}
#[test]
fn deploy_output_style_writes_all_styles() {
let home = tempdir().unwrap();
deploy_output_style(rc(), home.path()).expect("deploy succeeds");
let dir = home.path().join(".claude").join("output-styles");
for style in crate::core::bundle::OUTPUT_STYLES {
let path = dir.join(style.file_name);
assert!(path.exists(), "{} must be deployed", style.file_name);
let written = std::fs::read_to_string(&path).expect("style file readable");
assert_eq!(written, style.content(rc()), "{} content matches", style.id);
}
assert_eq!(crate::core::bundle::OUTPUT_STYLES.len(), 4);
}
#[test]
#[serial_test::serial]
fn prepare_session_reports_output_style() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let report = prepare_session(&fw, project).expect("prep succeeds");
let style = report
.output_style
.expect("output style deployed when home is resolvable");
assert!(style.ends_with("trusty-mpm.md"));
assert!(style.exists());
assert!(
style.starts_with(tmp_home.path()),
"output style must deploy under the injected FrameworkPaths base, got {}",
style.display()
);
}
#[test]
#[serial_test::serial]
fn prepare_session_reports_skill_deploy() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let report = prepare_session(&fw, project).expect("prep succeeds");
let _ = &report.skill_deploy;
}
#[test]
#[serial_test::serial]
fn prepare_session_is_idempotent() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let first = prepare_session(&fw, project).expect("first prep succeeds");
assert!(first.instructions.claude_md_created);
let second = prepare_session(&fw, project).expect("second prep succeeds");
assert!(
!second.instructions.claude_md_created,
"CLAUDE.md already exists on the second run"
);
}
fn seed_bundled_agents(fw: &crate::core::paths::FrameworkPaths) {
std::fs::create_dir_all(&fw.agents).unwrap();
std::fs::write(
fw.agents.join("base-engineer.md"),
"---\nname: base-engineer\nrole: base-engineer\n---\n\n# Base Eng\n\nBASE.\n",
)
.unwrap();
std::fs::write(
fw.agents.join("rust-engineer.md"),
"---\nname: rust-engineer\nrole: engineer\nextends: base-engineer\n---\n\n# Rust\n\nLEAF.\n",
)
.unwrap();
}
#[test]
#[serial_test::serial]
fn prepare_session_default_deploys_all_seeded_agents() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let mut fw = fw;
fw.trusty_mpm_root = None;
seed_bundled_agents(&fw);
let report = prepare_session(&fw, project).expect("prep succeeds");
assert!(
report
.deploy
.deployed
.contains(&"base-engineer.md".to_string())
);
assert!(
report
.deploy
.deployed
.contains(&"rust-engineer.md".to_string())
);
assert!(fw.agent_deploy_dir().join("rust-engineer.md").exists());
}
#[test]
#[serial_test::serial]
fn prepare_session_never_retracts_the_operator_home_agents_tier() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let mut fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
fw.trusty_mpm_root = None;
seed_bundled_agents(&fw);
let home_agents = fw.claude_agents_dir();
crate::core::agent_deployer::deploy_agents(&fw.agents, &home_agents, &fw.skill_deploy_dir())
.unwrap();
let before: Vec<String> = std::fs::read_dir(&home_agents)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert!(
before.iter().any(|n| n == "rust-engineer.md"),
"fixture must genuinely deploy into the home tier, else this test is vacuous"
);
let body_before = std::fs::read_to_string(home_agents.join("rust-engineer.md")).unwrap();
prepare_session(&fw, project).expect("prep succeeds");
assert!(
home_agents.is_dir(),
"prepare_session must not remove the operator's ~/.claude/agents directory"
);
let mut after: Vec<String> = std::fs::read_dir(&home_agents)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
let mut before_sorted = before;
before_sorted.sort();
after.sort();
assert_eq!(
after, before_sorted,
"prepare_session must leave the operator's ~/.claude/agents untouched"
);
assert_eq!(
std::fs::read_to_string(home_agents.join("rust-engineer.md")).unwrap(),
body_before,
"every file in the operator's home tier must survive byte-identical"
);
assert!(
home_agents
.join(crate::core::agent_manifest::MANIFEST_FILE)
.is_file(),
"the home tier's ownership manifest must survive too"
);
}
#[test]
#[serial_test::serial]
fn prepare_session_manifest_filters_agent_set() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let mut fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
fw.trusty_mpm_root = None;
seed_bundled_agents(&fw);
let manifest_dir = project.join(".trusty-mpm").join("framework");
std::fs::create_dir_all(&manifest_dir).unwrap();
std::fs::write(
manifest_dir.join("manifest.toml"),
"[agents]\ninclude = [\"rust-engineer\"]\n",
)
.unwrap();
let report = prepare_session(&fw, project).expect("prep succeeds");
assert!(
report
.deploy
.deployed
.contains(&"rust-engineer.md".to_string()),
"included agent must deploy"
);
assert!(
!report
.deploy
.deployed
.contains(&"base-engineer.md".to_string()),
"excluded-by-omission agent must NOT deploy"
);
assert!(!fw.agent_deploy_dir().join("base-engineer.md").exists());
}
#[test]
#[serial_test::serial]
fn prepare_session_manifest_sets_default_style() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let mut fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
fw.trusty_mpm_root = None;
let manifest_dir = project.join(".trusty-mpm").join("framework");
std::fs::create_dir_all(&manifest_dir).unwrap();
std::fs::write(
manifest_dir.join("manifest.toml"),
"[style]\nactive = \"trusty-mpm-research\"\n",
)
.unwrap();
prepare_session(&fw, project).expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm-research")
);
}
#[test]
#[serial_test::serial]
fn prepare_session_config_style_overrides_manifest() {
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let tmp = tempdir().unwrap();
let project = tmp.path();
let mut fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
fw.trusty_mpm_root = None;
std::fs::create_dir_all(&fw.root).unwrap();
std::fs::write(
fw.config_toml(),
"[style]\nactive = \"trusty-mpm-teacher\"\n",
)
.unwrap();
let manifest_dir = project.join(".trusty-mpm").join("framework");
std::fs::create_dir_all(&manifest_dir).unwrap();
std::fs::write(
manifest_dir.join("manifest.toml"),
"[style]\nactive = \"trusty-mpm-research\"\n",
)
.unwrap();
prepare_session(&fw, project).expect("prep succeeds");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap(),
)
.unwrap();
assert_eq!(
value["outputStyle"],
serde_json::json!("trusty-mpm-teacher"),
"config [style] active must override the manifest's style"
);
}
fn assert_resolved_statusline_command(cmd: &str) {
let binary = cmd
.strip_suffix(" statusline")
.unwrap_or_else(|| panic!("command must end with ' statusline', got {cmd}"));
assert!(
!trusty_common::bin_resolve::is_ephemeral_build_path(std::path::Path::new(binary)),
"resolved statusline binary must not be an ephemeral build path, got {binary}"
);
}
#[test]
fn ensure_status_line_in_seeds_both_tiers() {
let project = tempdir().unwrap();
let user = tempdir().unwrap();
let user_settings = user.path().join(".claude").join("settings.json");
super::ensure_status_line_in(project.path(), Some(&user_settings)).expect("seeding succeeds");
for path in [
project.path().join(".claude").join("settings.json"),
user_settings,
] {
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} must exist: {e}", path.display()));
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(v["statusLine"]["type"], "command", "{}", path.display());
assert_resolved_statusline_command(v["statusLine"]["command"].as_str().unwrap());
}
}
#[test]
fn ensure_status_line_in_without_a_user_tier_still_seeds_the_project() {
let project = tempdir().unwrap();
super::ensure_status_line_in(project.path(), None).expect("seeding succeeds");
let raw =
std::fs::read_to_string(project.path().join(".claude").join("settings.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(v["statusLine"]["type"], "command");
}
#[test]
fn write_status_line_injects_when_absent() {
let tmp = tempdir().unwrap();
write_status_line(tmp.path()).expect("write succeeds");
let raw = std::fs::read_to_string(tmp.path().join(".claude").join("settings.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(v["statusLine"]["type"], "command", "type must be command");
assert_resolved_statusline_command(v["statusLine"]["command"].as_str().unwrap());
assert_eq!(v["statusLine"]["padding"], 0, "padding must be 0");
}
#[test]
fn write_status_line_skips_when_already_set() {
let tmp = tempdir().unwrap();
let claude_dir = tmp.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let existing =
serde_json::json!({"statusLine": {"type": "command", "command": "my custom cmd"}});
std::fs::write(
claude_dir.join("settings.json"),
serde_json::to_string_pretty(&existing).unwrap(),
)
.unwrap();
write_status_line(tmp.path()).expect("write succeeds without modifying");
let raw = std::fs::read_to_string(claude_dir.join("settings.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(
v["statusLine"]["command"], "my custom cmd",
"existing statusLine must not be overwritten"
);
}
#[test]
fn write_status_line_preserves_user_config() {
let tmp = tempdir().unwrap();
let claude_dir = tmp.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let existing = serde_json::json!({
"outputStyle": "trusty-mpm-research",
"someKey": true,
"statusLine": {"type": "command", "command": "my-custom-statusline", "padding": 2}
});
std::fs::write(
claude_dir.join("settings.json"),
serde_json::to_string_pretty(&existing).unwrap(),
)
.unwrap();
write_status_line(tmp.path()).expect("write succeeds");
let raw = std::fs::read_to_string(claude_dir.join("settings.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(
v["outputStyle"], "trusty-mpm-research",
"outputStyle must be preserved"
);
assert_eq!(v["someKey"], true, "arbitrary keys must be preserved");
assert_eq!(
v["statusLine"]["command"], "my-custom-statusline",
"a genuinely custom statusLine.command must never be overwritten"
);
assert_eq!(
v["statusLine"]["padding"], 2,
"the rest of a custom statusLine entry must also survive untouched"
);
}
#[test]
fn write_status_line_heals_stale_tm_default() {
let tmp = tempdir().unwrap();
let claude_dir = tmp.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let existing = serde_json::json!({"statusLine": {"type": "command", "command": "tm statusline", "padding": 0}});
std::fs::write(
claude_dir.join("settings.json"),
serde_json::to_string_pretty(&existing).unwrap(),
)
.unwrap();
write_status_line(tmp.path()).expect("write succeeds");
let raw = std::fs::read_to_string(claude_dir.join("settings.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_resolved_statusline_command(v["statusLine"]["command"].as_str().unwrap());
assert_eq!(
v["statusLine"]["padding"], 0,
"padding must survive the in-place upgrade"
);
}
#[test]
fn write_status_line_heals_stale_trusty_mpm_default() {
let tmp = tempdir().unwrap();
let claude_dir = tmp.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let existing = serde_json::json!({
"statusLine": {"type": "command", "command": "trusty-mpm statusline", "padding": 0}
});
std::fs::write(
claude_dir.join("settings.json"),
serde_json::to_string_pretty(&existing).unwrap(),
)
.unwrap();
write_status_line(tmp.path()).expect("write succeeds");
let raw = std::fs::read_to_string(claude_dir.join("settings.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_resolved_statusline_command(v["statusLine"]["command"].as_str().unwrap());
}
#[test]
fn is_stale_bare_statusline_command_matches_known_defaults() {
assert!(is_stale_bare_statusline_command(
&serde_json::json!({"type": "command", "command": "tm statusline"})
));
assert!(is_stale_bare_statusline_command(
&serde_json::json!({"type": "command", "command": "trusty-mpm statusline"})
));
}
#[test]
fn is_stale_bare_statusline_command_ignores_custom_command() {
assert!(!is_stale_bare_statusline_command(
&serde_json::json!({"type": "command", "command": "tm statusline --compact"})
));
assert!(!is_stale_bare_statusline_command(
&serde_json::json!({"type": "command", "command": "my custom cmd"})
));
assert!(!is_stale_bare_statusline_command(
&serde_json::json!({"type": "command", "command": "/opt/homebrew/bin/tm statusline"})
));
}
#[test]
fn is_stale_bare_statusline_command_ignores_non_command_type() {
assert!(!is_stale_bare_statusline_command(
&serde_json::json!({"type": "text", "command": "tm statusline"})
));
}
#[test]
fn is_stale_statusline_command_flags_missing_and_ephemeral() {
assert!(is_stale_statusline_command(
&serde_json::json!({"type": "command", "command": "tm statusline"})
));
assert!(is_stale_statusline_command(&serde_json::json!({
"type": "command",
"command": "/repo/target/debug/deps/trusty_mpm-abc statusline"
})));
assert!(is_stale_statusline_command(&serde_json::json!({
"type": "command",
"command": "/repo/.claude/worktrees/fix-1/target/release/tm statusline"
})));
assert!(is_stale_statusline_command(&serde_json::json!({
"type": "command",
"command": "/no/such/dir/definitely-missing-2229 statusline"
})));
}
#[cfg(unix)]
#[test]
fn is_stale_statusline_command_respects_existing_custom_binary() {
assert!(!is_stale_statusline_command(&serde_json::json!({
"type": "command",
"command": "/bin/sh statusline"
})));
assert!(!is_stale_statusline_command(
&serde_json::json!({"type": "command", "command": "my-custom-line"})
));
assert!(!is_stale_statusline_command(&serde_json::json!({
"type": "text",
"command": "/no/such/dir/x statusline"
})));
}
#[test]
fn resolve_statusline_binary_with_prefers_current_exe() {
let resolved = resolve_statusline_binary_with(
|| Ok(PathBuf::from("/abs/path/to/tm")),
|_name| Some(PathBuf::from("/should/not/be/used/tm")),
);
assert_eq!(resolved, "/abs/path/to/tm");
}
#[test]
fn resolve_statusline_binary_with_rejects_ephemeral_current_exe() {
let resolved = resolve_statusline_binary_with(
|| Ok(PathBuf::from("/repo/target/debug/deps/trusty_mpm-abc123")),
|name| {
assert_eq!(
name, "tm",
"path lookup must search for the bare 'tm' name first"
);
Some(PathBuf::from("/opt/homebrew/bin/tm"))
},
);
assert_eq!(
resolved, "/opt/homebrew/bin/tm",
"an ephemeral current_exe must be rejected in favour of the PATH-resolved install"
);
}
#[test]
fn resolve_statusline_binary_with_rejects_system_temp_current_exe() {
let leaked: Vec<String> = [
PathBuf::from("/private/tmp/claude-502/-Users-x-proj/9f1c/scratchpad/base-bins/tm"),
PathBuf::from("/tmp/tm"),
std::env::temp_dir().join("claude-4485/base-bins/tm"),
]
.into_iter()
.map(|exe| {
resolve_statusline_binary_with(
move || Ok(exe.clone()),
|_name| Some(PathBuf::from("/opt/homebrew/bin/tm")),
)
})
.filter(|resolved| resolved != "/opt/homebrew/bin/tm")
.collect();
assert!(
leaked.is_empty(),
"a system temp current_exe must be rejected in favour of the PATH-resolved install \
(#4492), but these were persisted verbatim: {leaked:#?}"
);
}
#[test]
fn resolve_statusline_binary_with_falls_back_to_path_lookup() {
let resolved = resolve_statusline_binary_with(
|| Err(std::io::Error::other("current_exe unavailable")),
|name| {
assert_eq!(name, "tm", "path lookup must search for the bare 'tm' name");
Some(PathBuf::from("/opt/homebrew/bin/tm"))
},
);
assert_eq!(resolved, "/opt/homebrew/bin/tm");
}
#[test]
fn resolve_statusline_binary_with_falls_back_to_trusty_mpm_name() {
let resolved = resolve_statusline_binary_with(
|| Err(std::io::Error::other("current_exe unavailable")),
|name| match name {
"tm" => None,
"trusty-mpm" => Some(PathBuf::from("/opt/homebrew/bin/trusty-mpm")),
other => panic!("unexpected binary name looked up: {other}"),
},
);
assert_eq!(resolved, "/opt/homebrew/bin/trusty-mpm");
}
#[test]
fn resolve_statusline_binary_with_falls_back_to_bare_name() {
let resolved = resolve_statusline_binary_with(
|| Err(std::io::Error::other("current_exe unavailable")),
|_name| None,
);
assert_eq!(
resolved, "tm",
"must degrade to the bare literal when both sources fail"
);
}
#[test]
fn isolated_layout_deploys_into_a_tier_the_spawn_reads() {
let (flag, tier_list) = crate::core::model_inject::SETTING_SOURCES_FLAG
.split_once(' ')
.expect("SETTING_SOURCES_FLAG must be `--setting-sources <tiers>`");
assert_eq!(flag, "--setting-sources");
let tiers: Vec<&str> = tier_list.split(',').map(str::trim).collect();
assert!(
!tiers.is_empty() && !tiers.contains(&"user"),
"this invariant is only meaningful while the flag names tiers and excludes \
`user` (#1269); got {tiers:?}"
);
assert!(
tiers.contains(&"project"),
"the isolated deploy targets the project tier, so the flag must name it; \
got {tiers:?}"
);
let project_dir = std::path::Path::new("/work/some-checkout");
let fw = isolated_framework_paths(project_dir);
assert_eq!(
fw.claude_home_dir(),
project_dir,
"the deploy base must BE the harness cwd — anything else is the `user`-tier \
mismatch of #4203"
);
assert_eq!(
fw.claude_agents_dir(),
project_dir.join(".claude").join("agents"),
"agents must land under the harness cwd, not $HOME"
);
assert_eq!(
fw.claude_skills_dir(),
project_dir.join(".claude").join("skills"),
"skills must land under the harness cwd, not $HOME"
);
}
#[test]
#[serial_test::serial]
fn isolated_layout_keeps_framework_source_at_the_install_root() {
let project_dir = std::path::Path::new("/work/some-checkout");
let fw = isolated_framework_paths(project_dir);
assert_eq!(
fw.root,
FrameworkPaths::default().root,
"the framework install root must NOT move workspace-local"
);
assert!(
!fw.agent_source_dir().starts_with(project_dir),
"agent SOURCE must never resolve inside the deploy destination; got {}",
fw.agent_source_dir().display()
);
assert!(
!fw.skill_source_dir().starts_with(project_dir),
"skill SOURCE must never resolve inside the deploy destination; got {}",
fw.skill_source_dir().display()
);
}
#[test]
#[serial_test::serial]
fn resolve_palace_slug_falls_back_to_git_remote() {
let _env = EnvVarGuard::clear("TRUSTY_MEMORY_PALACE");
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("some-throwaway-dir");
std::fs::create_dir_all(&project).unwrap();
if !init_git_repo_with_origin(&project, "git@github.com:bobmatnyc/trusty-tools.git") {
eprintln!("skipping: git unavailable");
return;
}
assert_eq!(
resolve_palace_slug(&project, None).as_deref(),
Some("bobmatnyc-trusty-tools"),
"the slug must come from the origin remote, not the directory basename"
);
}
#[test]
#[serial_test::serial]
fn resolve_palace_slug_prefers_the_explicit_remote() {
let _env = EnvVarGuard::clear("TRUSTY_MEMORY_PALACE");
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("01H-session-id");
std::fs::create_dir_all(&project).unwrap();
assert_eq!(
resolve_palace_slug(&project, Some("git@github.com:acme/widget.git")).as_deref(),
Some("acme-widget")
);
}
#[test]
#[serial_test::serial]
fn resolve_palace_slug_override_env_wins() {
let _env = EnvVarGuard::set(
"TRUSTY_MEMORY_PALACE",
std::path::Path::new("operator-choice"),
);
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
resolve_palace_slug(tmp.path(), Some("git@github.com:acme/widget.git")).as_deref(),
Some("operator-choice")
);
}
fn plugin_index(config_dir: &std::path::Path, keys: &[&str]) {
let mut plugins = serde_json::Map::new();
for key in keys {
plugins.insert((*key).to_string(), serde_json::json!([]));
}
std::fs::create_dir_all(config_dir.join("plugins")).unwrap();
std::fs::write(
config_dir.join("plugins").join("installed_plugins.json"),
serde_json::to_string_pretty(&serde_json::json!({ "plugins": plugins })).unwrap(),
)
.unwrap();
}
fn read_settings(project: &std::path::Path) -> serde_json::Value {
let raw = std::fs::read_to_string(project.join(".claude").join("settings.json")).unwrap();
serde_json::from_str(&raw).unwrap()
}
#[test]
fn write_enabled_plugins_denies_a_non_opted_plugin() {
let tmp = tempdir().unwrap();
let project = tmp.path().join("repo");
let config_dir = tmp.path().join("cfg");
std::fs::create_dir_all(&project).unwrap();
plugin_index(&config_dir, &["aws-core@m", "vercel@m"]);
write_enabled_plugins(&project, Some(&config_dir)).unwrap();
let settings = read_settings(&project);
assert_eq!(
settings["enabledPlugins"]["aws-core@m"],
serde_json::json!(false)
);
assert_eq!(
settings["enabledPlugins"]["vercel@m"],
serde_json::json!(false)
);
}
fn declare_plugin_opt_in(project: &std::path::Path) {
std::fs::write(
project.join(crate::core::project_config::PROJECT_CONFIG_FILE),
"[session]\nplugins = [\"aws-core\"]\n",
)
.unwrap();
}
fn trust_project(project: &std::path::Path) {
let root = crate::core::project_trust::trust_store_root().expect("HOME was redirected");
let mut store = crate::core::project_trust::ProjectTrustStore::load(&root).unwrap();
store.trust(project);
store.save().unwrap();
}
#[test]
#[serial_test::serial]
fn write_enabled_plugins_enables_an_opted_in_plugin() {
let tmp = tempdir().unwrap();
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let project = tmp.path().join("repo");
let config_dir = tmp.path().join("cfg");
std::fs::create_dir_all(&project).unwrap();
plugin_index(&config_dir, &["aws-core@m", "vercel@m"]);
declare_plugin_opt_in(&project);
trust_project(&project);
write_enabled_plugins(&project, Some(&config_dir)).unwrap();
let settings = read_settings(&project);
assert_eq!(
settings["enabledPlugins"]["aws-core@m"],
serde_json::json!(true)
);
assert_eq!(
settings["enabledPlugins"]["vercel@m"],
serde_json::json!(false)
);
}
#[test]
#[serial_test::serial]
fn write_enabled_plugins_denies_an_opt_in_from_an_untrusted_project() {
let tmp = tempdir().unwrap();
let tmp_home = tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp_home.path());
let project = tmp.path().join("repo");
let config_dir = tmp.path().join("cfg");
std::fs::create_dir_all(&project).unwrap();
plugin_index(&config_dir, &["aws-core@m", "vercel@m"]);
declare_plugin_opt_in(&project);
write_enabled_plugins(&project, Some(&config_dir)).unwrap();
let settings = read_settings(&project);
assert_eq!(
settings["enabledPlugins"]["aws-core@m"],
serde_json::json!(false),
"an in-repo declaration is not its own permission"
);
assert_eq!(
settings["enabledPlugins"]["vercel@m"],
serde_json::json!(false)
);
}
#[test]
fn write_enabled_plugins_preserves_foreign_keys() {
let tmp = tempdir().unwrap();
let project = tmp.path().join("repo");
let config_dir = tmp.path().join("cfg");
std::fs::create_dir_all(project.join(".claude")).unwrap();
plugin_index(&config_dir, &["aws-core@m"]);
std::fs::write(
project.join(".claude").join("settings.json"),
serde_json::to_string_pretty(&serde_json::json!({
"outputStyle": "operator-choice",
"enabledPlugins": { "hand-added@local": true },
}))
.unwrap(),
)
.unwrap();
write_enabled_plugins(&project, Some(&config_dir)).unwrap();
let settings = read_settings(&project);
assert_eq!(
settings["outputStyle"],
serde_json::json!("operator-choice")
);
assert_eq!(
settings["enabledPlugins"]["hand-added@local"],
serde_json::json!(true),
"a key tm never enumerated is the operator's"
);
assert_eq!(
settings["enabledPlugins"]["aws-core@m"],
serde_json::json!(false)
);
}
#[test]
fn write_enabled_plugins_skips_without_a_config_dir() {
let tmp = tempdir().unwrap();
let project = tmp.path().join("repo");
std::fs::create_dir_all(&project).unwrap();
write_enabled_plugins(&project, None).unwrap();
assert!(
!project.join(".claude").join("settings.json").exists(),
"nothing to decide means nothing to write"
);
}