//! Environment profiles (issue #40). mod common; use agent_manager::cli::{Cli, Command, ProfileCmd, StartArgs}; use agent_manager::commands::{profile_cmd, run_cmd}; use clap::Parser; fn app_with_profiles(tag: &str) -> agent_manager::app::App { let dir = std::env::temp_dir().join(format!("am-profiles-{tag}-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let cfg = dir.join("config.yaml"); std::fs::write( &cfg, "version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\nprofiles:\n dev:\n agent: claude-code\n env:\n DEBUG: \"1\"\n LOG_LEVEL: debug\n args: [--model, sonnet]\n prod:\n env:\n LOG_LEVEL: info\nagents:\n - name: claude-code\n run: claude\n install: {type: npm, package: \"@anthropic-ai/claude-code\"}\n", ) .unwrap(); let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]); let mut app = agent_manager::app::App::from_cli(cli).unwrap(); let mut p = app.paths.clone(); p.install_dir = dir.join("agents"); p.bin_dir = dir.join("agents").join("bin"); p.log_dir = dir.join("logs"); p.state_file = dir.join("state.json"); p.probe_cache_file = dir.join("probe-cache.json"); p.config_dir = Some(dir.join("config")); app.paths = p; app } #[test] fn profile_list_and_show() { let app = app_with_profiles("list"); assert_eq!(profile_cmd::run_list(&app).unwrap(), 0); assert_eq!(profile_cmd::run_show(&app, "dev").unwrap(), 0); let json = app; // keep clippy quiet about unused paths let _ = json; } #[test] fn unknown_profile_lists_available_ones() { let app = app_with_profiles("unknown"); let err = profile_cmd::resolve(&app, "staging").unwrap_err().to_string(); assert!(err.contains("unknown profile 'staging'"), "{err}"); assert!(err.contains("dev") && err.contains("prod"), "{err}"); } #[test] fn apply_merges_profile_env_and_args() { let app = app_with_profiles("apply"); let (args, env) = profile_cmd::apply( &app, Some("prod"), &["--base".to_string()], &std::collections::BTreeMap::from([("KEEP".to_string(), "1".to_string())]), ) .unwrap(); assert_eq!(args, vec!["--base"]); assert_eq!(env.get("LOG_LEVEL").map(String::as_str), Some("info")); assert_eq!(env.get("KEEP").map(String::as_str), Some("1")); } #[test] fn start_with_unknown_profile_fails_cleanly() { let app = app_with_profiles("badstart"); let opts = StartArgs { agent: Some("claude-code".to_string()), profile: Some("staging".to_string()), ..Default::default() }; let err = run_cmd::start(&app, &opts).unwrap_err().to_string(); assert!(err.contains("unknown profile"), "{err}"); assert!(err.contains("dev"), "{err}"); } #[test] fn start_with_profile_dry_run_succeeds() { let mut app = app_with_profiles("dry"); // Force dry-run so start_one never spawns. app.cli.dry_run = true; let opts = StartArgs { agent: Some("claude-code".to_string()), profile: Some("dev".to_string()), ..Default::default() }; assert_eq!(run_cmd::start(&app, &opts).unwrap(), 0); } #[test] fn start_without_agent_uses_profile_default_agent() { let mut app = app_with_profiles("default"); app.cli.dry_run = true; let opts = StartArgs { agent: None, profile: Some("dev".to_string()), ..Default::default() }; assert_eq!(run_cmd::start(&app, &opts).unwrap(), 0); } #[test] fn cli_parses_profile_subcommands() { let cli = Cli::parse_from(["am", "profile", "list"]); assert!(matches!( cli.command, Some(Command::Profile(ProfileCmd::List)) )); let cli = Cli::parse_from(["am", "profile", "show", "dev"]); assert!(matches!( cli.command, Some(Command::Profile(ProfileCmd::Show { ref name })) if name == "dev" )); let cli = Cli::parse_from(["am", "start", "claude-code", "--profile", "dev"]); match cli.command { Some(Command::Start(a)) => assert_eq!(a.profile.as_deref(), Some("dev")), other => panic!("unexpected: {other:?}"), } }