//! i18n (issue #74): locale selection, English output, stable JSON contracts. //! //! The tests spawn the real am binary (CARGO_BIN_EXE_am) so the assertions //! cover the actual command output, not just the library layer. mod common; use agent_manager::i18n::{self, Lang}; use std::path::PathBuf; use std::process::Command; /// Run the real am binary with args and a temp config; returns /// (stdout, stderr, exit code). fn am(args: &[&str], config: &PathBuf) -> (String, String, i32) { let mut cmd = Command::new(env!("CARGO_BIN_EXE_am")); cmd.arg("--config").arg(config).args(args); let out = cmd.output().expect("am binary should run"); ( String::from_utf8_lossy(&out.stdout).to_string(), String::from_utf8_lossy(&out.stderr).to_string(), out.status.code().unwrap_or(-1), ) } /// A minimal valid config in a fresh temp dir (no user config leakage). fn temp_config(tag: &str) -> (PathBuf, PathBuf) { let dir = std::env::temp_dir().join(format!( "am-i18n-{tag}-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .subsec_nanos() )); 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\nagents: []\n", ) .unwrap(); (cfg, dir) } #[test] fn lang_flag_selects_english_for_tip_content() { let (cfg, _dir) = temp_config("flag"); // Explicit French: the tip page is French. let (fr, _, _) = am(&["--lang", "fr", "tip"], &cfg); assert!(fr.contains("Lancer & piloter"), "fr tip missing: {fr}"); // --lang en: the same page is translated. let (en, _, code) = am(&["--lang", "en", "tip"], &cfg); assert_eq!(code, 0); assert!(en.contains("Run & control"), "en tip missing: {en}"); assert!(!en.contains("Lancer & piloter"), "untranslated section: {en}"); // The flag works after the subcommand too (global clap flag). let (en2, _, _) = am(&["tip", "--lang", "en"], &cfg); assert!(en2.contains("Run & control"), "en tip (post) missing: {en2}"); } #[test] fn am_lang_env_selects_english() { let (cfg, _dir) = temp_config("env"); let mut cmd = Command::new(env!("CARGO_BIN_EXE_am")); cmd.arg("--config").arg(&cfg).arg("tip"); cmd.env("AM_LANG", "en"); let out = cmd.output().unwrap(); let text = String::from_utf8_lossy(&out.stdout).to_string(); assert!(text.contains("Run & control"), "AM_LANG=en ignored: {text}"); } #[test] fn lang_env_values_are_parsed_leniently() { // The host machine may export LANG/AM_LANG (git-bash sets LANG=en_GB): // clear them so the default resolution is exercised deterministically. std::env::remove_var("AM_LANG"); std::env::remove_var("LANG"); std::env::remove_var("LC_ALL"); std::env::remove_var("LC_MESSAGES"); assert_eq!(i18n::resolve(None), Lang::Fr); // no env -> default French std::env::set_var("AM_LANG", "en_US.UTF-8"); assert_eq!(i18n::resolve(None), Lang::En); std::env::remove_var("AM_LANG"); // The default follows the host LANG when set (spec: locale selon env). std::env::set_var("LANG", "fr_FR.UTF-8"); assert_eq!(i18n::resolve(None), Lang::Fr); } #[test] fn app_remembers_the_selected_language() { let app = common::test_app("applang", &["--lang", "en", "list"]); assert_eq!(app.lang(), Lang::En); assert!(i18n::active().is_en()); let app_fr = common::test_app("applangfr", &["--lang", "fr", "list"]); assert_eq!(app_fr.lang(), Lang::Fr); assert!(!i18n::active().is_en()); } #[test] fn json_contract_is_identical_across_languages() { // Issue #74 acceptance: --json field names and values are untouched by // the language switch — only display strings are translated. let (cfg, _dir) = temp_config("json"); let (fr, _, code_fr) = am(&["list", "--json"], &cfg); let (en, _, code_en) = am(&["list", "--json", "--lang", "en"], &cfg); assert_eq!(code_fr, 0); assert_eq!(code_en, 0); assert_eq!(fr, en, "list --json differs across languages"); // Same for status --json (structured detail). let (fr_s, _, _) = am(&["status", "--json"], &cfg); let (en_s, _, _) = am(&["status", "--json", "--lang", "en"], &cfg); assert_eq!(fr_s, en_s, "status --json differs across languages"); } #[test] fn init_reports_stack_in_english() { let (cfg, dir) = temp_config("init"); let work = dir.join("proj"); std::fs::create_dir_all(&work).unwrap(); let out = Command::new(env!("CARGO_BIN_EXE_am")) .arg("--config") .arg(&cfg) .args(["--lang", "en", "init", "--force"]) .current_dir(&work) .output() .unwrap(); let text = String::from_utf8_lossy(&out.stderr).to_string(); assert!( text.contains("none detected"), "EN init message missing: {text}" ); }