Files
agent-manager/tests/lab_plugins_test.rs
bruno 4c7866a25a feat: phase 2 — am lab (benchmark N agents) + plugins d'événements (contrat JSON)
- #62: am lab --agents a,b --task <f> — séquentiel ou --parallel, capture
  durée/exit/sortie/coût estimé, rapport comparatif tableau + --json stable,
  tâches versionnables dans <state>/lab/ (--list), rejouables à l'identique
- #75: plugins/ sous state_dir, déclenchés sur on_install/on_start/on_stop/
  on_update (App::emit), contrat JSON stdin/stdout, timeout configurable,
  échec non bloquant, garde anti-récursion AM_PLUGINS_RUNNING, am plugins
  + am plugins --test <name> (CI), exemples notify + ci-webhook
- intégration REPL (parse, complétion, bannière, is_am_command), help, tips,
  man pages (am-lab.1, am-plugins.1), README, ROADMAP (26/27), v0.5.4
- 334 tests verts (309 + 25)

closes #62
closes #75
2026-08-18 14:17:30 -04:00

170 lines
5.7 KiB
Rust

//! Integration tests: am lab (issue #62) and am plugins (issue #75).
mod common;
use agent_manager::cli::{Cli, LabArgs, PluginsArgs};
use agent_manager::commands::{lab_cmd, plugins_cmd};
use clap::Parser;
use std::path::PathBuf;
/// A config with two shell-based fake agents for the lab benchmark.
fn lab_config() -> String {
concat!(
"agents:\n",
" - name: bench-ok\n",
" installable: false\n",
" run: sh\n",
" args: [\"-c\", \"echo out: $0\"]\n",
" - name: bench-fail\n",
" installable: false\n",
" run: sh\n",
" args: [\"-c\", \"echo boom >&2; exit 3\"]\n",
)
.to_string()
}
#[test]
fn lab_runs_a_task_on_every_agent_and_reports() {
let dir = std::env::temp_dir().join(format!("am-lab-e2e-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cfg = common::write_config(&dir, &lab_config());
let task = dir.join("hello.yaml");
std::fs::write(
&task,
"name: hello\ndescription: e2e\nargs: [\"hello\"]\ntimeout: 30\n",
)
.unwrap();
let cli = Cli::parse_from([
"am",
"--config",
cfg.to_str().unwrap(),
"lab",
"--agents",
"bench-ok",
"--task",
"hello.yaml",
]);
let mut app = agent_manager::app::App::from_cli(cli).expect("app should build");
common::isolate(&mut app, &dir);
let code = lab_cmd::run(
&app,
&LabArgs {
agents: Some("bench-ok, bench-fail".to_string()),
task: Some(task.clone()),
parallel: false,
timeout: None,
list: false,
},
)
.unwrap();
// One agent fails → the exit code is 1 (CI-friendly).
assert_eq!(code, 1);
// The benchmark is replayable with the same file.
let code2 = lab_cmd::run(
&app,
&LabArgs {
agents: Some("bench-ok".to_string()),
task: Some(task),
parallel: false,
timeout: None,
list: false,
},
)
.unwrap();
assert_eq!(code2, 0);
// Each run was journaled as a lab event.
let events = agent_manager::events::read_events(&app.events_dir(), 0);
let lab_events = events.iter().filter(|e| e.kind == agent_manager::events::EventKind::Lab);
assert!(lab_events.count() >= 3, "expected >= 3 lab events");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lab_list_shows_the_tasks_of_the_lab_dir() {
let dir = std::env::temp_dir().join(format!("am-lab-list-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cfg = common::write_config(&dir, "agents: []\n");
let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap(), "lab", "--list"]);
let mut app = agent_manager::app::App::from_cli(cli).expect("app should build");
common::isolate(&mut app, &dir);
std::fs::create_dir_all(app.events_dir().join("lab")).unwrap();
std::fs::write(app.events_dir().join("lab/hello.yaml"), "name: hello\n").unwrap();
let code = lab_cmd::run(
&app,
&LabArgs {
agents: None,
task: None,
parallel: false,
timeout: None,
list: true,
},
)
.unwrap();
assert_eq!(code, 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn plugin_runs_on_an_emitted_event() {
let dir = std::env::temp_dir().join(format!("am-plugins-e2e-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cfg = common::write_config(&dir, "agents: []\n");
let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap(), "plugins"]);
let mut app = agent_manager::app::App::from_cli(cli).expect("app should build");
common::isolate(&mut app, &dir);
// Install an echo plugin: valid JSON on stdout (contract) + a marker
// file proving it ran. The absolute path keeps it independent of cwd.
let pdir = app.events_dir().join("plugins");
std::fs::create_dir_all(&pdir).unwrap();
let marker = pdir.join("pong.log");
let script = format!(
"#!/bin/sh\ncat >/dev/null\necho '{{\"ok\": true, \"message\": \"pong\"}}' | tee '{}'\n",
marker.display().to_string().replace('\\', "/")
);
std::fs::write(pdir.join("pong.sh"), script).unwrap();
let script_path = pdir.join("pong.sh").display().to_string().replace('\\', "/");
std::fs::write(pdir.join("pong.yaml"), format!("command: \"sh {script_path}\"\n")).unwrap();
// List: the plugin is discovered.
let code = plugins_cmd::run(&app, &PluginsArgs { test: None }).unwrap();
assert_eq!(code, 0);
// --test: the JSON contract is exercised.
let code = plugins_cmd::run(&app, &PluginsArgs { test: Some("pong".to_string()) }).unwrap();
assert_eq!(code, 0);
// A real emitted event fires the plugin (acceptance criterion).
app.emit(&agent_manager::events::Event::now(
agent_manager::events::EventKind::Start,
));
assert!(marker.exists(), "the plugin must have run on the emitted event");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn plugins_list_and_test_are_available_in_cli() {
let cli = Cli::parse_from(["am", "plugins", "--test", "notify"]);
match cli.command {
Some(agent_manager::cli::Command::Plugins(args)) => {
assert_eq!(args.test.as_deref(), Some("notify"));
}
other => panic!("unexpected: {other:?}"),
}
let cli = Cli::parse_from(["am", "lab", "--agents", "a,b", "--task", "t.yaml"]);
match cli.command {
Some(agent_manager::cli::Command::Lab(args)) => {
assert_eq!(args.agents.as_deref(), Some("a,b"));
assert_eq!(args.task, Some(PathBuf::from("t.yaml")));
}
other => panic!("unexpected: {other:?}"),
}
}