use clap::Args;
use std::fs;
use std::path::{Path, PathBuf};
use super::resolve_cwd;
use crate::config::Config;
use leviath_core::manifest::parse_manifest;
#[derive(Clone, Copy, PartialEq, Eq, Debug, clap::ValueEnum)]
pub enum ListFilter {
All,
Agents,
Blueprints,
}
impl ListFilter {
fn shows_agents(self) -> bool {
matches!(self, Self::All | Self::Agents)
}
fn shows_blueprints(self) -> bool {
matches!(self, Self::All | Self::Blueprints)
}
}
#[derive(Args)]
pub struct ListArgs {
#[arg(short, long, value_enum, default_value_t = ListFilter::All)]
pub filter: ListFilter,
#[arg(long)]
pub json: bool,
}
#[derive(serde::Serialize)]
pub(crate) struct AgentInfo {
pub(crate) name: String,
version: String,
pub(crate) description: String,
read_paths: Option<String>,
}
#[derive(serde::Serialize)]
pub(crate) struct ListedAgent {
#[serde(flatten)]
pub(crate) info: AgentInfo,
pub(crate) source: &'static str,
pub(crate) path: String,
}
#[derive(serde::Serialize)]
pub(crate) struct ListReport {
pub(crate) agents: Vec<ListedAgent>,
pub(crate) bundled: Vec<BundledEntry>,
}
#[derive(serde::Serialize)]
pub(crate) struct BundledEntry {
pub(crate) name: String,
pub(crate) version: String,
}
fn read_agent_info(manifest_path: &Path, config: &Config, cwd: &Path) -> Option<AgentInfo> {
let content = fs::read_to_string(manifest_path).ok()?;
let blueprint = parse_manifest(&content).ok()?;
let read_paths = read_path_summary(&blueprint, config, cwd);
Some(AgentInfo {
name: blueprint.name,
version: blueprint.version,
description: blueprint.description,
read_paths,
})
}
fn read_path_summary(
blueprint: &leviath_core::Blueprint,
config: &Config,
cwd: &Path,
) -> Option<String> {
match crate::read_path_report::build(blueprint, config, cwd)? {
Ok(report) if report.has_ungranted() => Some(format!(
"read_paths: {} - `lev validate` shows which",
report.summary()
)),
Ok(report) => Some(format!("read_paths: {}", report.summary())),
Err(e) => Some(format!("read_paths: {e}")),
}
}
fn scan_directory_for_agents(dir: &Path, config: &Config, cwd: &Path) -> Vec<(PathBuf, AgentInfo)> {
let mut agents = Vec::new();
if !dir.exists() {
return agents;
}
let direct_manifest = dir.join("agent.leviath");
if direct_manifest.exists()
&& let Some(info) = read_agent_info(&direct_manifest, config, cwd)
{
agents.push((dir.to_path_buf(), info));
}
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join("agent.leviath");
if manifest_path.exists()
&& let Some(info) = read_agent_info(&manifest_path, config, cwd)
{
agents.push((path, info));
}
}
}
}
agents
}
fn print_agent(info: &AgentInfo) {
let desc = if info.description.is_empty() {
String::new()
} else {
format!(" - {}", info.description)
};
println!(" {} (v{}){}", info.name, info.version, desc);
if let Some(read_paths) = &info.read_paths {
println!(" {read_paths}");
}
}
pub async fn execute(args: ListArgs) -> anyhow::Result<()> {
let config = Config::load()?;
let agents_dir = get_agents_dir()?;
let cwd = resolve_cwd().unwrap_or_default();
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
match args.json {
true => json_agent_listing(&agents_dir, &cwd, &config, args.filter),
false => print_agent_listing(&agents_dir, &cwd, exe_dir.as_deref(), &config, args.filter),
}
}
fn json_agent_listing(
agents_dir: &Path,
cwd: &Path,
config: &Config,
filter: ListFilter,
) -> anyhow::Result<()> {
let report = build_list_report(agents_dir, cwd, config, filter);
println!(
"{}",
serde_json::to_string_pretty(&report).expect("an agent listing serializes")
);
Ok(())
}
pub(crate) fn build_list_report(
agents_dir: &Path,
cwd: &Path,
config: &Config,
filter: ListFilter,
) -> ListReport {
if !filter.shows_agents() {
return ListReport {
agents: Vec::new(),
bundled: bundled_entries(),
};
}
let installed = scan_directory_for_agents(agents_dir, config, cwd);
let local = read_agent_info(&cwd.join("agent.leviath"), config, cwd);
let configured: Vec<(PathBuf, AgentInfo)> = config
.agent_paths
.iter()
.flat_map(|dir| scan_directory_for_agents(dir, config, cwd))
.collect();
let from = |entries: Vec<(PathBuf, AgentInfo)>, source| {
entries.into_iter().map(move |(path, info)| ListedAgent {
info,
source,
path: path.display().to_string(),
})
};
let mut agents: Vec<ListedAgent> = from(installed, "installed")
.chain(from(configured, "configured"))
.collect();
if let Some(info) = local {
agents.push(ListedAgent {
info,
source: "local",
path: cwd.join("agent.leviath").display().to_string(),
});
}
ListReport {
agents,
bundled: match filter.shows_blueprints() {
true => bundled_entries(),
false => Vec::new(),
},
}
}
fn bundled_entries() -> Vec<BundledEntry> {
crate::bundled::BUNDLED_AGENTS
.iter()
.map(|a| BundledEntry {
name: a.name.to_string(),
version: a.version.to_string(),
})
.collect()
}
fn print_agent_listing(
agents_dir: &Path,
cwd: &Path,
exe_dir: Option<&Path>,
config: &Config,
filter: ListFilter,
) -> anyhow::Result<()> {
let mut found_runnable = false;
let installed = match filter.shows_agents() {
true => scan_directory_for_agents(agents_dir, config, cwd),
false => Vec::new(),
};
if !installed.is_empty() {
found_runnable = true;
println!("Installed agents (~/.leviath/agents/):");
for (_path, info) in &installed {
print_agent(info);
}
println!();
}
let local_manifest = cwd.join("agent.leviath");
if filter.shows_agents()
&& local_manifest.exists()
&& let Some(info) = read_agent_info(&local_manifest, config, cwd)
{
found_runnable = true;
println!("Local (current directory):");
print_agent(&info);
println!();
}
let mut config_agents = Vec::new();
if filter.shows_agents() {
for agent_path in &config.agent_paths {
let found = scan_directory_for_agents(agent_path, config, cwd);
config_agents.extend(found);
}
}
if !config_agents.is_empty() {
found_runnable = true;
println!("From configured paths:");
for (_path, info) in &config_agents {
print_agent(info);
}
println!();
}
let mut builtin_names: Vec<String> = match filter.shows_blueprints() {
true => crate::bundled::BUNDLED_AGENTS
.iter()
.map(|a| format!("{} (v{})", a.name, a.version))
.collect(),
false => Vec::new(),
};
if filter.shows_blueprints()
&& let Some(exe_dir) = exe_dir
{
for (_path, info) in scan_directory_for_agents(&exe_dir.join("agents"), config, cwd) {
let entry = format!("{} (v{})", info.name, info.version);
if !builtin_names.contains(&entry) {
builtin_names.push(entry);
}
}
}
if filter.shows_blueprints() {
println!("Bundled agents (install with `lev setup`):");
println!(" {}", builtin_names.join(", "));
println!();
}
if filter.shows_agents() && !found_runnable {
println!("No agent blueprints installed yet.");
println!();
println!("To install the bundled agent blueprints:");
println!(" lev setup");
println!();
println!("To create your own:");
println!(" lev create my-agent");
}
Ok(())
}
fn get_agents_dir_or_error(dir: Option<PathBuf>) -> anyhow::Result<PathBuf> {
dir.ok_or(anyhow::anyhow!("Could not determine home directory"))
}
fn get_agents_dir() -> anyhow::Result<PathBuf> {
#[cfg(test)]
if FORCE_AGENTS_DIR_ERROR.with(|f| f.get()) {
anyhow::bail!("Could not determine home directory");
}
get_agents_dir_or_error(leviath_core::paths::agents_dir())
}
#[cfg(test)]
thread_local! {
static FORCE_AGENTS_DIR_ERROR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::write_test_agent;
fn write_manifest(dir: &Path, name: &str) {
write_manifest_with_description(dir, name, "Test agent");
}
fn read_agent_info(manifest_path: &Path) -> Option<AgentInfo> {
super::read_agent_info(manifest_path, &Config::default(), Path::new("/work"))
}
fn scan_directory_for_agents(dir: &Path) -> Vec<(PathBuf, AgentInfo)> {
super::scan_directory_for_agents(dir, &Config::default(), Path::new("/work"))
}
fn write_manifest_with_description(dir: &Path, name: &str, description: &str) {
let content = format!(
r#"[agent]
name = "{}"
version = "1.0.0"
description = "{}"
[stages.main]
mode = "autonomous"
model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
description = "Main"
max_iterations = 5
[context.regions]
system = {{ kind = "pinned", max_tokens = 1000 }}
"#,
name, description
);
write_test_agent(dir, content);
}
fn write_read_paths_manifest(dir: &Path, name: &str) {
let content = format!(
r#"[agent]
name = "{name}"
version = "1.0.0"
description = "Test agent"
[stages.main]
mode = "autonomous"
model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
description = "Main"
max_iterations = 5
[context.regions]
system = {{ kind = "pinned", max_tokens = 1000 }}
[read_paths]
allow = ["/data/runs"]
"#
);
write_test_agent(dir, content);
}
fn info_with_config(dir: &Path, config: &Config) -> AgentInfo {
super::read_agent_info(&dir.join("agent.leviath"), config, Path::new("/work"))
.expect("manifest parses")
}
#[test]
fn an_ungranted_read_paths_declaration_is_listed_as_such() {
let dir = tempfile::tempdir().unwrap();
write_read_paths_manifest(dir.path(), "cto");
let summary = info_with_config(dir.path(), &Config::default())
.read_paths
.expect("declares read paths");
assert!(summary.contains("1 declared, 0 granted"), "{summary}");
assert!(summary.contains("lev validate"), "{summary}");
}
#[test]
fn a_granted_read_paths_declaration_needs_no_pointer() {
let dir = tempfile::tempdir().unwrap();
write_read_paths_manifest(dir.path(), "cto");
let mut config = Config::default();
config.security.read_paths = vec!["/data/runs".to_string()];
let summary = info_with_config(dir.path(), &config)
.read_paths
.expect("declares read paths");
assert_eq!(summary, "read_paths: 1 declared, 1 granted");
}
#[test]
fn a_broken_grant_list_is_reported_on_the_agent() {
let dir = tempfile::tempdir().unwrap();
write_read_paths_manifest(dir.path(), "cto");
let mut config = Config::default();
config.security.read_paths = vec!["regex:relative/.*".to_string()];
let summary = info_with_config(dir.path(), &config)
.read_paths
.expect("declares read paths");
assert!(summary.contains("config.toml"), "{summary}");
}
#[test]
fn an_agent_declaring_no_read_paths_gets_no_line() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "plain");
assert!(
info_with_config(dir.path(), &Config::default())
.read_paths
.is_none()
);
}
#[test]
fn read_agent_info_valid_manifest() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "my-agent");
let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
assert_eq!(info.name, "my-agent");
assert_eq!(info.version, "1.0.0");
assert_eq!(info.description, "Test agent");
}
#[test]
fn read_agent_info_missing_file_returns_none() {
let result = read_agent_info(Path::new("/nonexistent/agent.leviath"));
assert!(result.is_none());
}
#[test]
fn read_agent_info_invalid_toml_returns_none() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
let result = read_agent_info(&dir.path().join("agent.leviath"));
assert!(result.is_none());
}
#[test]
fn scan_directory_nonexistent_returns_empty() {
let agents = scan_directory_for_agents(Path::new("/nonexistent/path"));
assert!(agents.is_empty());
}
#[test]
fn scan_directory_path_is_a_file_returns_empty() {
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("not-a-directory.txt");
fs::write(&file_path, "hello").unwrap();
let agents = scan_directory_for_agents(&file_path);
assert!(agents.is_empty());
}
#[test]
fn scan_directory_direct_manifest_invalid_is_skipped() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn scan_directory_with_direct_manifest() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "direct-agent");
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1.name, "direct-agent");
}
#[test]
fn scan_directory_with_subdirectories() {
let dir = tempfile::tempdir().unwrap();
let sub1 = dir.path().join("agent-a");
let sub2 = dir.path().join("agent-b");
fs::create_dir_all(&sub1).unwrap();
fs::create_dir_all(&sub2).unwrap();
write_manifest(&sub1, "agent-a");
write_manifest(&sub2, "agent-b");
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 2);
let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
assert!(names.contains(&"agent-a"));
assert!(names.contains(&"agent-b"));
}
#[test]
fn scan_directory_ignores_subdirs_without_manifest() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("no-manifest");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("readme.txt"), "not a manifest").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
fn report_under(filter: ListFilter) -> ListReport {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let sub = agents_dir.path().join("installed-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "installed-agent");
build_list_report(agents_dir.path(), cwd.path(), &Config::default(), filter)
}
#[test]
fn filter_agents_reports_agents_and_no_blueprints() {
let report = report_under(ListFilter::Agents);
assert!(!report.agents.is_empty(), "runnable agents are the point");
assert!(report.bundled.is_empty(), "blueprints were not asked for");
}
#[test]
fn filter_blueprints_reports_blueprints_and_no_agents() {
let report = report_under(ListFilter::Blueprints);
assert!(report.agents.is_empty(), "agents were not asked for");
assert!(!report.bundled.is_empty(), "the catalog is never empty");
}
#[test]
fn filter_all_reports_both() {
let report = report_under(ListFilter::All);
assert!(!report.agents.is_empty());
assert!(!report.bundled.is_empty());
}
#[test]
fn an_excluded_half_is_present_and_empty_in_json() {
let json = serde_json::to_value(report_under(ListFilter::Agents)).unwrap();
assert!(
json.get("bundled")
.is_some_and(|b| b.as_array().is_some_and(Vec::is_empty))
);
let json = serde_json::to_value(report_under(ListFilter::Blueprints)).unwrap();
assert!(
json.get("agents")
.is_some_and(|a| a.as_array().is_some_and(Vec::is_empty))
);
}
#[test]
fn an_unknown_filter_is_refused() {
use clap::Parser;
#[derive(Parser)]
struct Cli {
#[command(flatten)]
args: ListArgs,
}
assert!(Cli::try_parse_from(["lev", "--filter", "agants"]).is_err());
let ok = Cli::try_parse_from(["lev", "--filter", "agents"]).expect("a known spelling");
assert_eq!(ok.args.filter, ListFilter::Agents);
}
#[test]
fn list_args_default_filter() {
let args = ListArgs {
filter: ListFilter::All,
json: false,
};
assert_eq!(args.filter, ListFilter::All);
}
#[test]
fn read_agent_info_extracts_description() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "my-agent");
let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
assert_eq!(info.description, "Test agent");
assert_eq!(info.version, "1.0.0");
}
#[test]
fn scan_directory_with_both_direct_and_subdirs() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "root-agent");
let sub = dir.path().join("child");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "child-agent");
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 2);
let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
assert!(names.contains(&"root-agent"));
assert!(names.contains(&"child-agent"));
}
#[test]
fn scan_directory_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn scan_directory_subdir_with_invalid_manifest() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("bad-agent");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("agent.leviath"), "invalid toml {{{{").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn get_agents_dir_returns_path_with_agents() {
let dir = get_agents_dir().unwrap();
assert!(dir.to_str().unwrap().contains(".leviath"));
assert!(dir.to_str().unwrap().ends_with("agents"));
}
#[test]
fn get_agents_dir_or_error_some_returns_path() {
let dir = PathBuf::from("/home/testuser/.leviath/agents");
assert_eq!(get_agents_dir_or_error(Some(dir.clone())).unwrap(), dir);
}
#[test]
fn get_agents_dir_or_error_none_returns_error() {
let err = get_agents_dir_or_error(None).unwrap_err();
assert!(
err.to_string()
.contains("Could not determine home directory")
);
}
#[test]
fn read_agent_info_minimal_manifest() {
let dir = tempfile::tempdir().unwrap();
let content = r#"[agent]
name = "minimal"
version = "0.0.1"
description = ""
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_test_agent(dir.path(), content);
let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
assert_eq!(info.name, "minimal");
assert_eq!(info.description, "");
}
#[tokio::test]
async fn execute_runs_without_error() {
crate::config::with_isolated_config_path_async("list-runs-ok", |_fake_dir| async move {
let args = ListArgs {
filter: ListFilter::All,
json: false,
};
let result = execute(args).await;
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn execute_returns_err_when_agents_dir_unresolvable() {
crate::config::with_isolated_config_path_async("list-dir-err", |_fake_dir| async move {
FORCE_AGENTS_DIR_ERROR.with(|f| f.set(true));
let args = ListArgs {
filter: ListFilter::All,
json: false,
};
let result = execute(args).await;
FORCE_AGENTS_DIR_ERROR.with(|f| f.set(false));
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("Could not determine home directory")
);
})
.await;
}
#[cfg(unix)]
#[tokio::test]
async fn execute_falls_back_to_default_cwd_when_current_dir_is_gone() {
crate::config::with_isolated_config_path_async("list-cwd-gone", |_fake_dir| async move {
let _guard = crate::config::isolate_cwd_for_test();
let dir = std::env::temp_dir().join("lev-test-list-cwd-gone");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
let args = ListArgs {
filter: ListFilter::All,
json: false,
};
let result = execute(args).await;
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn execute_falls_back_to_default_cwd_via_forced_error() {
crate::config::with_isolated_config_path_async("list-cwd-forced", |_fake_dir| async move {
crate::commands::force_cwd_error(true);
let args = ListArgs {
filter: ListFilter::All,
json: false,
};
let result = execute(args).await;
crate::commands::force_cwd_error(false);
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn execute_fails_loudly_on_a_broken_config() {
crate::config::with_isolated_config_path_async(
"list-broken-config",
|fake_dir| async move {
std::fs::write(fake_dir.join("config.toml"), "not = valid = toml").unwrap();
let args = ListArgs {
filter: ListFilter::All,
json: false,
};
let err = execute(args).await.expect_err("broken config must error");
assert!(err.to_string().contains("parse"), "{err}");
},
)
.await;
}
#[test]
fn json_listing_tags_each_agent_with_where_it_came_from() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let configured = tempfile::tempdir().unwrap();
let installed = agents_dir.path().join("from-install");
fs::create_dir_all(&installed).unwrap();
write_manifest(&installed, "installed-agent");
write_manifest(cwd.path(), "local-agent");
let extra = configured.path().join("from-config");
fs::create_dir_all(&extra).unwrap();
write_manifest(&extra, "configured-agent");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let report = build_list_report(agents_dir.path(), cwd.path(), &config, ListFilter::All);
let sourced: Vec<(&str, &str)> = report
.agents
.iter()
.map(|a| (a.info.name.as_str(), a.source))
.collect();
assert!(sourced.contains(&("installed-agent", "installed")));
assert!(sourced.contains(&("configured-agent", "configured")));
assert!(sourced.contains(&("local-agent", "local")));
}
#[test]
fn json_listing_reports_the_bundled_catalog_separately_from_runnable_agents() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let report = build_list_report(
agents_dir.path(),
cwd.path(),
&Config::default(),
ListFilter::All,
);
assert!(report.agents.is_empty());
assert_eq!(report.bundled.len(), crate::bundled::BUNDLED_AGENTS.len());
}
#[test]
fn json_listing_flattens_the_agent_fields_next_to_its_source() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
write_manifest(cwd.path(), "flat-agent");
let report = build_list_report(
agents_dir.path(),
cwd.path(),
&Config::default(),
ListFilter::All,
);
let value: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&report).unwrap()).unwrap();
assert_eq!(value["agents"][0]["name"], serde_json::json!("flat-agent"));
assert_eq!(value["agents"][0]["source"], serde_json::json!("local"));
assert!(value["agents"][0]["path"].is_string());
}
#[tokio::test]
async fn execute_with_json_runs_without_error() {
crate::config::with_isolated_config_path_async("list-json-ok", |_fake_dir| async move {
let args = ListArgs {
filter: ListFilter::All,
json: true,
};
assert!(execute(args).await.is_ok());
})
.await;
}
#[test]
fn print_agent_listing_nothing_installed() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let config = Config::default();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_honours_each_filter() {
let agents_dir = tempfile::tempdir().unwrap();
let sub = agents_dir.path().join("installed-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "installed-agent");
let cwd = tempfile::tempdir().unwrap();
let configured = tempfile::tempdir().unwrap();
let other = configured.path().join("configured-agent");
fs::create_dir_all(&other).unwrap();
write_manifest(&other, "configured-agent");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
for filter in [ListFilter::All, ListFilter::Agents, ListFilter::Blueprints] {
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config, filter);
assert!(result.is_ok(), "{filter:?}");
}
}
#[test]
fn print_agent_listing_skips_the_exe_dir_scan_for_agents_only() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let exe_dir = tempfile::tempdir().unwrap();
let bundled = exe_dir.path().join("agents").join("side-loaded");
fs::create_dir_all(&bundled).unwrap();
write_manifest(&bundled, "side-loaded");
for filter in [ListFilter::All, ListFilter::Agents, ListFilter::Blueprints] {
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
Some(exe_dir.path()),
&Config::default(),
filter,
);
assert!(result.is_ok(), "{filter:?}");
}
}
#[test]
fn print_agent_listing_finds_installed_agent() {
let agents_dir = tempfile::tempdir().unwrap();
let sub = agents_dir.path().join("installed-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "installed-agent");
let cwd = tempfile::tempdir().unwrap();
let config = Config::default();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_local_manifest() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
write_manifest(cwd.path(), "local-agent");
let config = Config::default();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_local_manifest_invalid_is_skipped() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
fs::write(cwd.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
let config = Config::default();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_configured_path_agent() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let configured = tempfile::tempdir().unwrap();
let sub = configured.path().join("configured-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "configured-agent");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_builtin_agents() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let exe_dir = tempfile::tempdir().unwrap();
let builtin_dir = exe_dir.path().join("agents");
let sub = builtin_dir.join("builtin-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "builtin-agent");
let config = Config::default();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
Some(exe_dir.path()),
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_carries_the_read_paths_line() {
let agents_dir = tempfile::tempdir().unwrap();
let agent = agents_dir.path().join("cto");
fs::create_dir_all(&agent).unwrap();
write_read_paths_manifest(&agent, "cto");
let cwd = tempfile::tempdir().unwrap();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&Config::default(),
ListFilter::All,
);
assert!(result.is_ok());
assert!(
info_with_config(&agent, &Config::default())
.read_paths
.is_some()
);
}
#[test]
fn print_agent_listing_does_not_list_a_bundled_agent_twice() {
let bundled = &crate::bundled::BUNDLED_AGENTS[0];
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let exe_dir = tempfile::tempdir().unwrap();
let sub = exe_dir.path().join("agents").join(bundled.name);
fs::create_dir_all(&sub).unwrap();
crate::bundled::install_bundled(bundled, &exe_dir.path().join("agents")).unwrap();
let config = Config::default();
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
Some(exe_dir.path()),
&config,
ListFilter::All,
);
assert!(result.is_ok());
let entry = format!("{} (v{})", bundled.name, bundled.version);
let names: Vec<String> = crate::bundled::BUNDLED_AGENTS
.iter()
.map(|a| format!("{} (v{})", a.name, a.version))
.collect();
assert_eq!(names.iter().filter(|n| **n == entry).count(), 1);
}
#[test]
fn print_agent_listing_all_sources_populated() {
let agents_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
write_manifest(&agents_dir.path().join("installed"), "installed");
let cwd = tempfile::tempdir().unwrap();
write_manifest(cwd.path(), "local");
let configured = tempfile::tempdir().unwrap();
fs::create_dir_all(configured.path().join("configured")).unwrap();
write_manifest(&configured.path().join("configured"), "configured");
let exe_dir = tempfile::tempdir().unwrap();
let builtin_sub = exe_dir.path().join("agents").join("builtin");
fs::create_dir_all(&builtin_sub).unwrap();
write_manifest(&builtin_sub, "builtin");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
Some(exe_dir.path()),
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_empty_descriptions_across_all_sources() {
let agents_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
write_manifest_with_description(&agents_dir.path().join("installed"), "installed", "");
let cwd = tempfile::tempdir().unwrap();
write_manifest_with_description(cwd.path(), "local", "");
let configured = tempfile::tempdir().unwrap();
fs::create_dir_all(configured.path().join("configured")).unwrap();
write_manifest_with_description(&configured.path().join("configured"), "configured", "");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let result = print_agent_listing(
agents_dir.path(),
cwd.path(),
None,
&config,
ListFilter::All,
);
assert!(result.is_ok());
}
#[test]
fn scan_directory_agent_with_empty_description() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("my-agent");
fs::create_dir_all(&sub).unwrap();
let content = r#"[agent]
name = "my-agent"
version = "2.0.0"
description = ""
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_test_agent(sub, content);
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1.description, "");
}
#[test]
fn scan_directory_mixed_valid_and_invalid() {
let dir = tempfile::tempdir().unwrap();
let good = dir.path().join("good");
let bad = dir.path().join("bad");
let empty = dir.path().join("empty");
fs::create_dir_all(&good).unwrap();
fs::create_dir_all(&bad).unwrap();
fs::create_dir_all(&empty).unwrap();
write_manifest(&good, "good-agent");
fs::write(bad.join("agent.leviath"), "bad {{ toml").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1.name, "good-agent");
}
}