//! Instrumentation: every real action appends one event to the journal; //! dry runs write nothing. Uses the same isolated-App pattern as the other //! integration tests (no user config or state leaks in). mod common; use agent_manager::app::App; use agent_manager::cli::{Cli, StartArgs}; use agent_manager::commands::run_cmd; use agent_manager::events::{self, Event, EventKind}; use agent_manager::state::InstalledEntry; use clap::Parser; use std::ffi::OsString; use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; static COUNTER: AtomicU32 = AtomicU32::new(0); /// Build an isolated App whose catalog contains the given agent definition. fn app_with_agent(tag: &str, args: &[&str], agent_yaml: &str) -> App { let id = COUNTER.fetch_add(1, Ordering::SeqCst); let dir = std::env::temp_dir().join(format!("am-instr-{tag}-{}-{id}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let cfg = common::write_config(&dir, agent_yaml); let mut full = vec!["am".to_string(), "--config".to_string(), cfg.display().to_string()]; full.extend(args.iter().map(|s| s.to_string())); let cli = Cli::parse_from(full); let mut app = App::from_cli(cli).expect("app should build"); common::isolate(&mut app, &dir); app } fn events(app: &App) -> Vec { events::read_events(&app.events_dir(), 0) } const ECHO_AGENT: &str = r#" agents: - name: echo-agent install: { type: binary, url: "https://example.com/echo.zip" } run: sh args: ["-c", "echo hi"] "#; #[test] fn run_emits_one_run_event_with_exit_code() { let app = app_with_agent("run", &["run", "echo-agent"], ECHO_AGENT); let code = run_cmd::run(&app, "echo-agent", None, None, false, false, &[OsString::from("hello")]).unwrap(); assert_eq!(code, 0); let evs = events(&app); assert_eq!(evs.len(), 1, "exactly one event expected, got {:?}", evs); assert_eq!(evs[0].kind, EventKind::Run); assert_eq!(evs[0].agent.as_deref(), Some("echo-agent")); assert_eq!(evs[0].exit_code, Some(0)); assert!(evs[0].duration_s.is_some()); } #[test] fn dry_run_writes_no_event() { let app = app_with_agent("dry", &["run", "echo-agent", "--dry-run"], ECHO_AGENT); let code = run_cmd::run(&app, "echo-agent", None, None, false, false, &[]).unwrap(); assert_eq!(code, 0); assert!(events(&app).is_empty(), "dry-run must not append events"); assert!(events::journal_files(&app.events_dir()).is_empty()); } fn sleeper_agent() -> String { let (run, args) = if cfg!(windows) { ("ping", r#"["-n", "60", "127.0.0.1"]"#) } else { ("sleep", r#"["60"]"#) }; format!( r#" agents: - name: sleeper install: {{ type: binary, url: "https://example.com/sleeper.zip" }} run: {run} args: {args} "# ) } #[test] fn start_and_stop_emit_a_matching_pair() { let app = app_with_agent("startstop", &["start", "sleeper", "--background"], &sleeper_agent()); // The agent must be "managed" for the PID to be recorded and stoppable. let entry = InstalledEntry { name: "sleeper".to_string(), version: None, method: "binary".to_string(), install_dir: app.paths.install_dir.display().to_string(), bins: vec![], run: "sleeper".to_string(), installed_at: "2026-01-01T00:00:00Z".to_string(), updated_at: None, pid: None, started_at: None, }; app.state.set(&entry).unwrap(); let opts = StartArgs { agent: Some("sleeper".to_string()), background: true, ..Default::default() }; let code = run_cmd::start(&app, &opts).unwrap(); assert_eq!(code, 0); let stop_code = run_cmd::stop(&app, "sleeper", false, None).unwrap(); assert_eq!(stop_code, 0); let evs = events(&app); assert_eq!(evs.len(), 2, "expected Start + Stop, got {:?}", evs); assert_eq!(evs[0].kind, EventKind::Start); assert_eq!(evs[0].agent.as_deref(), Some("sleeper")); assert!(evs[0].pid.is_some()); assert!(evs[0].session.is_some(), "start events carry a session id"); assert_eq!(evs[1].kind, EventKind::Stop); assert_eq!(evs[1].pid, evs[0].pid); assert_eq!(evs[1].exit_code, Some(0)); assert!(evs[1].duration_s.is_some(), "stop events carry the duration"); } #[test] fn fixture_path_is_reachable() { // Guard: the shared fixture used by events_test stays parsable here too. let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/events-sample.jsonl"); assert_eq!(events::parse_file(&p).len(), 2); }