use clap::Parser;
use crate::cli::{
Cli, CliCompressionLevel, Command, OptimizerAction, OverseerAction, SessionAction,
};
use crate::commands::install::{
deploy_report_lines, install_to, mpm_hook_additions, skill_report_lines,
write_project_hooks_for_dir,
};
use crate::commands::misc::{DISABLE_HOOKS_ENV, SUB_AGENT_ENV, hook};
use crate::commands::project::scaffold_project_dir;
use crate::formatters::session::{event_summary, print_compression_stats};
#[test]
fn project_init_scaffolds_dotdir() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().join("my-app");
std::fs::create_dir_all(&project).unwrap();
let report = scaffold_project_dir(&project).unwrap();
assert_eq!(report.len(), 2);
let config = project.join(".trusty-mpm/config.toml");
let sessions = project.join(".trusty-mpm/sessions");
assert!(config.exists());
assert!(sessions.is_dir());
let contents = std::fs::read_to_string(&config).unwrap();
assert!(contents.contains("name = \"my-app\""));
assert!(contents.contains("[agents]"));
assert!(contents.contains("[skills]"));
}
#[test]
fn project_init_keeps_existing_config() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().to_path_buf();
scaffold_project_dir(&project).unwrap();
let config = project.join(".trusty-mpm/config.toml");
std::fs::write(&config, "# edited by hand").unwrap();
let report = scaffold_project_dir(&project).unwrap();
assert!(report.iter().any(|l| l.contains("skipped")));
assert_eq!(
std::fs::read_to_string(&config).unwrap(),
"# edited by hand"
);
}
#[test]
fn cli_parses_install_no_force() {
let cli = Cli::try_parse_from(["trusty-mpm", "install"]).unwrap();
match cli.command.unwrap() {
Command::Install { force, .. } => assert!(!force),
other => panic!("expected Install, got {other:?}"),
}
}
#[test]
fn cli_parses_install_with_force() {
let cli = Cli::try_parse_from(["trusty-mpm", "install", "--force"]).unwrap();
match cli.command.unwrap() {
Command::Install { force, .. } => assert!(force),
other => panic!("expected Install, got {other:?}"),
}
}
#[test]
fn cli_parses_hook() {
let cli = Cli::try_parse_from(["trusty-mpm", "hook"]).unwrap();
assert!(matches!(
cli.command.unwrap(),
Command::Hook { pm_guard: false }
));
}
#[test]
fn cli_parses_hook_pm_guard() {
let cli = Cli::try_parse_from(["trusty-mpm", "hook", "--pm-guard"]).unwrap();
assert!(matches!(
cli.command.unwrap(),
Command::Hook { pm_guard: true }
));
}
#[test]
fn cli_parses_compress() {
let cli = Cli::try_parse_from(["trusty-mpm", "compress", "--tool", "bash"]).unwrap();
match cli.command.unwrap() {
Command::Compress { tool } => assert_eq!(tool, "bash"),
other => panic!("expected Compress, got {other:?}"),
}
}
#[tokio::test]
async fn hook_guard_short_circuits() {
unsafe {
std::env::set_var(SUB_AGENT_ENV, "1");
}
let client = reqwest::Client::new();
let started = std::time::Instant::now();
let res = hook(&client, "http://127.0.0.1:1").await;
let elapsed = started.elapsed();
unsafe {
std::env::remove_var(SUB_AGENT_ENV);
}
assert!(res.is_ok(), "guard branch must return Ok");
assert!(
elapsed < std::time::Duration::from_millis(500),
"guard must short-circuit without network I/O; took {elapsed:?}"
);
}
#[tokio::test]
async fn hook_disable_env_short_circuits() {
unsafe {
std::env::set_var(DISABLE_HOOKS_ENV, "1");
}
let client = reqwest::Client::new();
let started = std::time::Instant::now();
let res = hook(&client, "http://127.0.0.1:1").await;
let elapsed = started.elapsed();
unsafe {
std::env::remove_var(DISABLE_HOOKS_ENV);
}
assert!(res.is_ok(), "DISABLE_HOOKS_ENV branch must return Ok");
assert!(
elapsed < std::time::Duration::from_millis(500),
"DISABLE_HOOKS_ENV must short-circuit without network I/O; took {elapsed:?}"
);
}
#[test]
fn test_write_project_hooks_for_dir_targets_project_dir() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path();
let settings = project_dir.join(".claude").join("settings.json");
assert!(!settings.exists());
let wrote = write_project_hooks_for_dir(project_dir).unwrap();
assert!(wrote, "must report file was written");
assert!(settings.exists(), "project settings must be created");
let text = std::fs::read_to_string(&settings).unwrap();
let val: serde_json::Value = serde_json::from_str(&text).unwrap();
let hooks = val.get("hooks").expect("hooks key must be present");
assert!(
hooks.get("PreToolUse").is_some(),
"PreToolUse hook must be written"
);
let cmd = hooks["PreToolUse"][0]["hooks"][0]["command"]
.as_str()
.unwrap();
assert!(
cmd.ends_with(" hook"),
"hook command must end with ' hook', got: {cmd:?}"
);
}
#[test]
fn mpm_hook_additions_is_idempotent_under_merge() {
use trusty_common::claude_config::merge_hook_entries;
let additions = mpm_hook_additions();
let once = merge_hook_entries(&serde_json::json!({}), &additions);
let twice = merge_hook_entries(&once, &additions);
assert_eq!(once, twice, "second merge must be a no-op");
for event in ["PreToolUse", "PostToolUse", "Stop"] {
let arr = once["hooks"][event].as_array().expect("event array");
assert_eq!(arr.len(), 1, "{event}: expected one matcher block");
}
}
#[test]
fn install_writes_all_artifacts() {
let dir = tempfile::tempdir().unwrap();
let paths = trusty_mpm::core::paths::FrameworkPaths::under(dir.path());
let report = install_to(&paths, false).unwrap();
assert_eq!(report.len(), trusty_mpm::core::bundle::ALL.len());
for artifact in trusty_mpm::core::bundle::ALL {
let dest = paths.framework.join(artifact.rel_path);
assert!(dest.exists(), "missing {}", artifact.rel_path);
let written = std::fs::read_to_string(&dest).unwrap();
assert_eq!(written, artifact.contents);
}
}
#[test]
#[ignore = "requires bundled framework artifacts (base-agent) — run locally after tm install"]
fn install_then_deploy_composes_agents() {
let dir = tempfile::tempdir().unwrap();
let paths = trusty_mpm::core::paths::FrameworkPaths::under(dir.path());
install_to(&paths, false).unwrap();
let result = trusty_mpm::core::agent_deployer::deploy_agents(
&paths.agent_source_dir(),
&paths.claude_agents_dir(),
)
.unwrap();
assert_eq!(result.deployed.len(), 6);
assert!(result.skipped.is_empty());
let engineer = std::fs::read_to_string(paths.claude_agents_dir().join("engineer.md")).unwrap();
assert!(engineer.contains("BASE-AGENT"));
assert!(engineer.contains("BASE-ENGINEER"));
assert!(engineer.contains("# Engineer"));
assert!(!engineer.contains("extends:"));
let lines = deploy_report_lines(&result, &paths.agent_source_dir());
assert!(
lines
.iter()
.any(|l| l.contains("engineer.md") && l.contains("composed:")),
"lines = {lines:?}"
);
}
#[test]
fn install_then_deploy_deploys_skills() {
let dir = tempfile::tempdir().unwrap();
let paths = trusty_mpm::core::paths::FrameworkPaths::under(dir.path());
install_to(&paths, false).unwrap();
let result = trusty_mpm::core::skill_tiers::deploy_all_skill_tiers(
&paths.skill_source_dir(),
&paths.user_skill_source_dir(),
&paths.claude_skills_dir(),
|_| true,
)
.unwrap()
.stats;
for expected in [
"tm-circuit-breaker",
"tm-doctor",
"tm-issues-prune",
"tm-cli-operations",
"tm-capabilities",
"documentation-style",
] {
assert!(
result.deployed.contains(&expected.to_string()),
"{expected} must be deployed; got {:?}",
result.deployed
);
}
assert_eq!(
result.deployed.len(),
131,
"expected 131 skill files deployed (19 /tm- portfolio + tm-doctor + tm overview \
+ code-review-standards + contract-driven-testing + 93 skill-port batch-1 entries \
+ 7 documentation-style entries + 7 tm-capabilities entries \
+ 1 rust-build-performance entry); got {:?}",
result.deployed
);
assert!(result.skipped.is_empty());
assert!(result.unchanged.is_empty());
let deployed = paths
.claude_skills_dir()
.join("tm-circuit-breaker")
.join("SKILL.md");
assert!(
deployed.is_file(),
"expected skill at {}",
deployed.display()
);
let lines = skill_report_lines(&result);
assert!(
lines.iter().any(|l| l.contains("tm-circuit-breaker")),
"lines = {lines:?}"
);
}
#[test]
fn cli_parses_optimizer_status() {
let cli = Cli::try_parse_from(["trusty-mpm", "optimizer", "status"]).unwrap();
assert!(matches!(
cli.command.unwrap(),
Command::Optimizer {
action: OptimizerAction::Status
}
));
}
#[test]
fn cli_parses_optimizer_set() {
let cli = Cli::try_parse_from(["trusty-mpm", "optimizer", "set", "trim"]).unwrap();
match cli.command.unwrap() {
Command::Optimizer {
action: OptimizerAction::Set { level },
} => assert!(matches!(level, CliCompressionLevel::Trim)),
other => panic!("expected optimizer set, got {other:?}"),
}
}
#[test]
fn cli_parses_session_events() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "events", "abc-123"]).unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Events { id_or_name },
} => assert_eq!(id_or_name, "abc-123"),
other => panic!("expected session events, got {other:?}"),
}
}
#[test]
fn cli_parses_session_breakers() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "breakers"]).unwrap();
assert!(matches!(
cli.command.unwrap(),
Command::Session {
action: SessionAction::Breakers
}
));
}
#[test]
fn cli_parses_session_pause() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "pause", "tmpm-quiet-falcon"]).unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Pause { id_or_name, note },
} => {
assert_eq!(id_or_name, "tmpm-quiet-falcon");
assert_eq!(note, None);
}
other => panic!("expected session pause, got {other:?}"),
}
}
#[test]
fn cli_parses_session_pause_with_note() {
let cli = Cli::try_parse_from([
"trusty-mpm",
"session",
"pause",
"abc-123",
"--note",
"stepping away",
])
.unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Pause { id_or_name, note },
} => {
assert_eq!(id_or_name, "abc-123");
assert_eq!(note.as_deref(), Some("stepping away"));
}
other => panic!("expected session pause, got {other:?}"),
}
}
#[test]
fn cli_parses_session_resume() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "resume", "abc-123"]).unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Resume { id_or_name },
} => assert_eq!(id_or_name, "abc-123"),
other => panic!("expected session resume, got {other:?}"),
}
}
#[test]
fn cli_parses_session_run() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "run", "abc-123", "help me"]).unwrap();
match cli.command.unwrap() {
Command::Session {
action:
SessionAction::Run {
id_or_name,
command,
summarize,
},
} => {
assert_eq!(id_or_name, "abc-123");
assert_eq!(command, "help me");
assert!(!summarize);
}
other => panic!("expected session run, got {other:?}"),
}
}
#[test]
fn cli_parses_session_run_with_summarize() {
let cli = Cli::try_parse_from([
"trusty-mpm",
"session",
"run",
"tmpm-abc",
"help",
"--summarize",
])
.unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Run { summarize, .. },
} => assert!(summarize),
other => panic!("expected session run, got {other:?}"),
}
}
#[test]
fn cli_parses_session_output_defaults() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "output", "abc-123"]).unwrap();
match cli.command.unwrap() {
Command::Session {
action:
SessionAction::Output {
id_or_name,
lines,
summarize,
},
} => {
assert_eq!(id_or_name, "abc-123");
assert_eq!(lines, 50);
assert!(!summarize);
}
other => panic!("expected session output, got {other:?}"),
}
}
#[test]
fn cli_parses_session_output_with_lines() {
let cli = Cli::try_parse_from([
"trusty-mpm",
"session",
"output",
"abc-123",
"--lines",
"120",
])
.unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Output { lines, .. },
} => assert_eq!(lines, 120),
other => panic!("expected session output, got {other:?}"),
}
}
#[test]
fn cli_parses_session_output_with_summarize() {
let cli = Cli::try_parse_from(["trusty-mpm", "session", "output", "tmpm-abc", "--summarize"])
.unwrap();
match cli.command.unwrap() {
Command::Session {
action: SessionAction::Output { summarize, .. },
} => assert!(summarize),
other => panic!("expected session output, got {other:?}"),
}
}
#[test]
fn compression_stats_line_skipped_when_uncompressed() {
let body = serde_json::json!({
"output": "raw",
"compress_level": serde_json::Value::Null,
});
assert!(
body.get("compress_level")
.and_then(|v| v.as_str())
.is_none()
);
print_compression_stats(&body);
}
#[test]
fn compression_stats_line_printed_when_compressed() {
let body = serde_json::json!({
"output": "summary",
"original_bytes": 4096,
"compressed_bytes": 312,
"compress_level": "summarise",
});
assert_eq!(
body.get("compress_level").and_then(|v| v.as_str()),
Some("summarise")
);
print_compression_stats(&body);
}
#[test]
fn cli_session_pause_requires_arg() {
assert!(Cli::try_parse_from(["trusty-mpm", "session", "pause"]).is_err());
}
#[test]
fn cli_session_run_requires_command() {
assert!(Cli::try_parse_from(["trusty-mpm", "session", "run", "abc-123"]).is_err());
}
#[test]
fn cli_parses_overseer_status() {
let cli = Cli::try_parse_from(["trusty-mpm", "overseer", "status"]).unwrap();
assert!(matches!(
cli.command.unwrap(),
Command::Overseer {
action: OverseerAction::Status
}
));
}
#[test]
fn event_summary_prefers_tool_field() {
let payload = serde_json::json!({"tool": "Bash", "input": {"command": "ls"}});
assert_eq!(event_summary(&payload), "tool=Bash");
}
#[test]
fn event_summary_truncates_long_payloads() {
let payload = serde_json::json!({"k": "x".repeat(200)});
let summary = event_summary(&payload);
assert!(summary.chars().count() <= 61);
assert!(summary.ends_with('\u{2026}'));
}