events : instrumentation start/stop/run/install/update/uninstall/doctor/repl/shell (issue #3) - un evenement JSONL par action reelle, dry-run sans evenement, session id sur start, duree et exit code sur stop/run
This commit is contained in:
@@ -29,7 +29,7 @@ fn append_then_read_back_through_read_events() {
|
||||
log.append(
|
||||
&Event::now(EventKind::Run)
|
||||
.with_agent("jcode")
|
||||
.with_cwd("/tmp/demo")
|
||||
.with_cwd(Some("/tmp/demo".to_string()))
|
||||
.with_exit_code(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//! 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<Event> {
|
||||
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", &[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", &[]).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: "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);
|
||||
}
|
||||
Reference in New Issue
Block a user