feat: am tip cheat sheet + readable man pages + REPL completion refresh
This commit is contained in:
@@ -86,6 +86,138 @@ fn agents_command_lists_installed_only_in_installed_mode() {
|
||||
assert_eq!(agents_cmd::run(&app, true).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_command_filters_aliases_and_groups_by_launchable_target() {
|
||||
let dir = std::env::temp_dir().join(format!("am-test-alias-group-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let cfg = common::write_config(
|
||||
&dir,
|
||||
r#"
|
||||
agents:
|
||||
- name: claude-code
|
||||
display_name: "Claude Code"
|
||||
category: coding-agent
|
||||
install:
|
||||
type: npm
|
||||
package: "@anthropic-ai/claude-code"
|
||||
run: claude
|
||||
- name: github-copilot
|
||||
display_name: "GitHub Copilot"
|
||||
category: coding-agent
|
||||
install:
|
||||
type: npm
|
||||
package: "@github/copilot"
|
||||
run: copilot
|
||||
- name: aider
|
||||
display_name: "Aider"
|
||||
category: coding-agent
|
||||
install:
|
||||
type: uv
|
||||
package: aider-install
|
||||
run: aider
|
||||
aliases:
|
||||
cc: claude-code
|
||||
copilot: github-copilot
|
||||
groups:
|
||||
dev: [claude-code, aider]
|
||||
empty: [github-copilot]
|
||||
"#,
|
||||
);
|
||||
let cli = Cli::parse_from([
|
||||
"am",
|
||||
"--config",
|
||||
cfg.to_str().unwrap(),
|
||||
"list",
|
||||
]);
|
||||
let mut app = agent_manager::app::App::from_cli(cli).expect("app should build");
|
||||
common::isolate(&mut app, &dir);
|
||||
|
||||
let entry = InstalledEntry {
|
||||
name: "claude-code".to_string(),
|
||||
version: Some("1.0.0".to_string()),
|
||||
method: "npm".to_string(),
|
||||
install_dir: dir.join("claude-code").display().to_string(),
|
||||
bins: vec![],
|
||||
run: "claude".to_string(),
|
||||
installed_at: "2026-08-01T00:00:00Z".to_string(),
|
||||
updated_at: None,
|
||||
pid: None,
|
||||
started_at: None,
|
||||
};
|
||||
app.state.set(&entry).unwrap();
|
||||
|
||||
let names = agents_cmd::agent_names(&app, true);
|
||||
assert!(
|
||||
names.contains(&"claude-code".to_string()),
|
||||
"installed agent must appear: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.contains(&"cc".to_string()),
|
||||
"alias pointing to an installed agent must appear: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.contains(&"group:dev".to_string()),
|
||||
"group with at least one installed member must appear: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
!names.contains(&"github-copilot".to_string()),
|
||||
"agent that is not installed must not appear: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
!names.contains(&"copilot".to_string()),
|
||||
"alias pointing to a non-installed agent must not appear: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
!names.contains(&"group:empty".to_string()),
|
||||
"group with no installed members must not appear: {names:?}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilt_completer_tracks_install_and_uninstall() {
|
||||
// The REPL completer snapshots the agent pool at startup; the line
|
||||
// editor is rebuilt after install/uninstall, and a rebuilt completer
|
||||
// must reflect the state file: a fresh install shows up under Tab and
|
||||
// an uninstall disappears — without restarting am.
|
||||
let app = common::test_app("refresh", &["list"]);
|
||||
let before = agent_manager::repl::AmCompleter::new(&app);
|
||||
let (_, cands_before) = before.candidates_for("start ", 6);
|
||||
assert!(
|
||||
!cands_before.contains(&"pi".to_string()),
|
||||
"pi must not be proposed before its install: {cands_before:?}"
|
||||
);
|
||||
|
||||
app.state
|
||||
.set(&InstalledEntry {
|
||||
name: "pi".to_string(),
|
||||
version: Some("0.84.2".to_string()),
|
||||
method: "binary".to_string(),
|
||||
install_dir: "/tmp/pi".to_string(),
|
||||
bins: vec![],
|
||||
run: "pi".to_string(),
|
||||
installed_at: "2026-08-17T00:00:00Z".to_string(),
|
||||
updated_at: None,
|
||||
pid: None,
|
||||
started_at: None,
|
||||
})
|
||||
.unwrap();
|
||||
let after_install = agent_manager::repl::AmCompleter::new(&app);
|
||||
let (_, cands_installed) = after_install.candidates_for("start ", 6);
|
||||
assert!(
|
||||
cands_installed.contains(&"pi".to_string()),
|
||||
"a rebuilt completer must propose a freshly installed agent: {cands_installed:?}"
|
||||
);
|
||||
|
||||
app.state.remove("pi").unwrap();
|
||||
let after_uninstall = agent_manager::repl::AmCompleter::new(&app);
|
||||
let (_, cands_removed) = after_uninstall.candidates_for("start ", 6);
|
||||
assert!(
|
||||
!cands_removed.contains(&"pi".to_string()),
|
||||
"a rebuilt completer must drop an uninstalled agent: {cands_removed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn man_pages_are_written_to_output_dir() {
|
||||
let app = common::test_app("man", &["man"]);
|
||||
@@ -94,7 +226,7 @@ fn man_pages_are_written_to_output_dir() {
|
||||
let code = man_cmd::run(&app, None, Some(&dir)).unwrap();
|
||||
assert_eq!(code, 0);
|
||||
assert!(dir.join("am.1").exists(), "am.1 missing");
|
||||
for name in ["start", "sessions", "stats", "log", "history", "dashboard", "favorite", "profile", "man"] {
|
||||
for name in ["start", "sessions", "stats", "log", "history", "dashboard", "favorite", "profile", "man", "tip"] {
|
||||
assert!(
|
||||
dir.join(format!("am-{name}.1")).exists(),
|
||||
"am-{name}.1 missing"
|
||||
@@ -109,6 +241,28 @@ fn man_pages_are_written_to_output_dir() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn man_page_renders_to_readable_text() {
|
||||
// The roff source of the real CLI page must render to readable text:
|
||||
// macros and font escapes gone, sections and option terms kept.
|
||||
let cmd = Cli::command();
|
||||
let man = clap_mangen::Man::new(cmd);
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
man.render(&mut buf).unwrap();
|
||||
let roff = String::from_utf8_lossy(&buf);
|
||||
let text = man_cmd::roff_to_text(&roff, agent_manager::theme::default_theme(), false);
|
||||
assert!(text.contains("▸ NAME"), "NAME section missing:\n{text}");
|
||||
assert!(text.contains("▸ SYNOPSIS"), "SYNOPSIS section missing");
|
||||
assert!(text.contains("▸ OPTIONS"), "OPTIONS section missing");
|
||||
assert!(text.contains("▸ SUBCOMMANDS"), "SUBCOMMANDS section missing");
|
||||
assert!(!text.contains(".SH"), "macros must be removed: {text}");
|
||||
assert!(!text.contains("\\fB"), "font escapes must be converted: {text}");
|
||||
// Option terms appear before their descriptions (term/body order).
|
||||
let pos_config = text.find("--config <FILE>").unwrap_or(0);
|
||||
let pos_desc = text.find("Use an alternate configuration file").unwrap_or(0);
|
||||
assert!(pos_config < pos_desc, "term must precede its body:\n{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_man_command_is_rejected() {
|
||||
let app = common::test_app("manbad", &["man"]);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! tip: the cheat sheet page (render, JSON, --random, REPL completion).
|
||||
|
||||
mod common;
|
||||
|
||||
use agent_manager::cli::Cli;
|
||||
use agent_manager::commands::tip_cmd;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn tip_page_renders_header_sections_and_options() {
|
||||
let app = common::test_app("tip", &["tip"]);
|
||||
let text = tip_cmd::render(&app, false);
|
||||
assert!(text.contains("TIP"), "header missing:\n{text}");
|
||||
assert!(text.contains("antisèche"), "subtitle missing:\n{text}");
|
||||
for title in [
|
||||
"Lancer & piloter",
|
||||
"Installer & maintenir",
|
||||
"Observer",
|
||||
"Personnaliser",
|
||||
"Boîte à outils",
|
||||
] {
|
||||
assert!(text.contains(title), "missing section '{title}':\n{text}");
|
||||
}
|
||||
assert!(text.contains("start <agent>"), "missing usage line");
|
||||
assert!(text.contains("--background"), "missing option");
|
||||
assert!(text.contains("Le saviez-vous"), "missing tip of the day");
|
||||
assert!(
|
||||
text.contains("Vos commandes les plus utilisées"),
|
||||
"missing personal section"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tip_json_output_is_structured() {
|
||||
let app = common::test_app("tip-json", &["tip", "--json"]);
|
||||
let text = tip_cmd::render(&app, false);
|
||||
let v: serde_json::Value = serde_json::from_str(&text).expect("valid JSON output");
|
||||
assert!(v["sections"].is_array(), "sections missing");
|
||||
assert_eq!(v["sections"].as_array().unwrap().len(), 5);
|
||||
assert!(v["tip"]["title"].is_string());
|
||||
assert!(v["tip"]["body"].is_string());
|
||||
assert!(v["most_used"].is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tip_random_prints_a_single_tip() {
|
||||
let app = common::test_app("tip-rnd", &["tip", "--random"]);
|
||||
let text = tip_cmd::render(&app, true);
|
||||
assert!(text.contains("💡"), "random tip missing: {text}");
|
||||
assert!(!text.contains("antisèche"), "random mode must not print the page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tip_run_succeeds() {
|
||||
let app = common::test_app("tip-run", &["tip"]);
|
||||
assert_eq!(tip_cmd::run(&app, false).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_parses_tip_flags() {
|
||||
let cli = Cli::parse_from(["am", "tip", "--random"]);
|
||||
match cli.command {
|
||||
Some(agent_manager::cli::Command::Tip { random }) => assert!(random, "--random must be parsed"),
|
||||
other => panic!("unexpected command: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_completion_proposes_tip() {
|
||||
let app = common::test_app("tip-comp", &["list"]);
|
||||
let c = agent_manager::repl::AmCompleter::new(&app);
|
||||
let (_, cands) = c.candidates_for("ti", 2);
|
||||
assert!(cands.contains(&"tip".to_string()), "candidates: {cands:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user