//! top, report and log --follow (issues #37 #38). mod common; use agent_manager::commands::stats_cmd; use agent_manager::events; fn app_with_events(tag: &str, lines: &[&str]) -> agent_manager::app::App { let app = common::test_app(tag, &["stats"]); let month = chrono::Utc::now().format("%Y%m").to_string(); let path = app.events_dir().join(format!("events-{month}.jsonl")); std::fs::create_dir_all(app.events_dir()).unwrap(); let mut text = String::new(); for l in lines { text.push_str(l); text.push('\n'); } std::fs::write(&path, text).unwrap(); app } #[test] fn top_sorts_by_launches() { let app = app_with_events( "top", &[ r#"{"ts":"2026-08-01T10:00:00Z","kind":"run","agent":"a","exit_code":0}"#, r#"{"ts":"2026-08-02T10:00:00Z","kind":"run","agent":"a","exit_code":0}"#, r#"{"ts":"2026-08-03T10:00:00Z","kind":"run","agent":"b","exit_code":0}"#, ], ); let rows = stats_cmd::top(&app, None).unwrap(); assert_eq!(rows.len(), 2); assert_eq!(rows[0].agent, "a"); assert_eq!(rows[0].launches, 2); } #[test] fn report_md_contains_numbers_and_table() { let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); let lines = [ format!(r#"{{"ts":"{today}T10:00:00Z","kind":"run","agent":"a","exit_code":0,"duration_s":10}}"#), format!(r#"{{"ts":"{today}T10:01:00Z","kind":"run","agent":"a","exit_code":1,"duration_s":5}}"#), format!(r#"{{"ts":"{today}T10:02:00Z","kind":"install","agent":"b"}}"#), ]; let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect(); let app = app_with_events("report", &refs); // Issue #74: the report follows the app language; force French so the // assertions are deterministic whatever the host LANG says. app.set_lang(agent_manager::i18n::Lang::Fr); let md = stats_cmd::report_md(&app, 7); assert!(md.contains("# Rapport agent-manager"), "md: {md}"); assert!(md.contains("Lancements : 2")); assert!(md.contains("Échecs : 1")); assert!(md.contains("Changements")); assert!(md.contains("| a | 2 |"), "table top: {md}"); } #[test] fn report_command_writes_file() { let app = app_with_events("reportcmd", &[]); let out = app.events_dir().join("report-test.md"); let code = stats_cmd::run_report(&app, true, false, Some(&out)).unwrap(); assert_eq!(code, 0); assert!(out.exists()); let _ = std::fs::remove_file(&out); } #[test] fn text_after_is_utf8_safe() { use agent_manager::commands::log_cmd; let text = "héllo wörld second line "; assert_eq!(log_cmd::text_after(text, 0), text); assert_eq!(log_cmd::text_after(text, 3), "llo wörld second line "); let full = text.len(); assert_eq!(log_cmd::text_after(text, full), ""); } #[test] fn follow_flag_parses() { use agent_manager::cli::{Cli, Command}; use clap::Parser; let cli = Cli::parse_from(["am", "log", "--follow"]); match &cli.command { Some(Command::Log { follow, .. }) => assert!(*follow), other => panic!("unexpected: {other:?}"), } } #[test] fn journal_still_reads_after_events_api() { let app = app_with_events("api", &[]); let mut log = events::EventLog::new(app.events_dir()); log.append(&events::Event::now(events::EventKind::Run).with_agent("x")).unwrap(); assert_eq!(events::read_events(&app.events_dir(), 0).len(), 1); }