1use clap::Args;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use super::resolve_cwd;
8use crate::config::Config;
9use leviath_core::manifest::parse_manifest;
10
11#[derive(Args)]
12pub struct ListArgs {
13 #[arg(short, long, default_value = "all")]
15 pub filter: String,
16
17 #[arg(long)]
20 pub json: bool,
21}
22
23#[derive(serde::Serialize)]
25struct AgentInfo {
26 name: String,
27 version: String,
28 description: String,
29 read_paths: Option<String>,
34}
35
36#[derive(serde::Serialize)]
39struct ListedAgent {
40 #[serde(flatten)]
41 info: AgentInfo,
42 source: &'static str,
44 path: String,
45}
46
47#[derive(serde::Serialize)]
49struct ListReport {
50 agents: Vec<ListedAgent>,
52 bundled: Vec<BundledEntry>,
55}
56
57#[derive(serde::Serialize)]
58struct BundledEntry {
59 name: String,
60 version: String,
61}
62
63fn read_agent_info(manifest_path: &Path, config: &Config, cwd: &Path) -> Option<AgentInfo> {
64 let content = fs::read_to_string(manifest_path).ok()?;
65 let blueprint = parse_manifest(&content).ok()?;
66 let read_paths = read_path_summary(&blueprint, config, cwd);
67 Some(AgentInfo {
68 name: blueprint.name,
69 version: blueprint.version,
70 description: blueprint.description,
71 read_paths,
72 })
73}
74
75fn read_path_summary(
79 blueprint: &leviath_core::Blueprint,
80 config: &Config,
81 cwd: &Path,
82) -> Option<String> {
83 match crate::read_path_report::build(blueprint, config, cwd)? {
84 Ok(report) if report.has_ungranted() => Some(format!(
85 "read_paths: {} - `lev validate` shows which",
86 report.summary()
87 )),
88 Ok(report) => Some(format!("read_paths: {}", report.summary())),
89 Err(e) => Some(format!("read_paths: {e}")),
90 }
91}
92
93fn scan_directory_for_agents(dir: &Path, config: &Config, cwd: &Path) -> Vec<(PathBuf, AgentInfo)> {
94 let mut agents = Vec::new();
95 if !dir.exists() {
96 return agents;
97 }
98
99 let direct_manifest = dir.join("agent.leviath");
101 if direct_manifest.exists()
102 && let Some(info) = read_agent_info(&direct_manifest, config, cwd)
103 {
104 agents.push((dir.to_path_buf(), info));
105 }
106
107 if let Ok(entries) = fs::read_dir(dir) {
109 for entry in entries.flatten() {
110 let path = entry.path();
111 if path.is_dir() {
112 let manifest_path = path.join("agent.leviath");
113 if manifest_path.exists()
114 && let Some(info) = read_agent_info(&manifest_path, config, cwd)
115 {
116 agents.push((path, info));
117 }
118 }
119 }
120 }
121
122 agents
123}
124
125fn print_agent(info: &AgentInfo) {
128 let desc = if info.description.is_empty() {
129 String::new()
130 } else {
131 format!(" - {}", info.description)
132 };
133 println!(" {} (v{}){}", info.name, info.version, desc);
134 if let Some(read_paths) = &info.read_paths {
135 println!(" {read_paths}");
136 }
137}
138
139pub async fn execute(args: ListArgs) -> anyhow::Result<()> {
140 let config = Config::load()?;
144 let agents_dir = get_agents_dir()?;
145 let cwd = resolve_cwd().unwrap_or_default();
146 let exe_dir = std::env::current_exe()
147 .ok()
148 .and_then(|p| p.parent().map(|p| p.to_path_buf()));
149
150 match args.json {
151 true => json_agent_listing(&agents_dir, &cwd, &config),
152 false => print_agent_listing(&agents_dir, &cwd, exe_dir.as_deref(), &config),
153 }
154}
155
156fn json_agent_listing(agents_dir: &Path, cwd: &Path, config: &Config) -> anyhow::Result<()> {
163 let report = build_list_report(agents_dir, cwd, config);
164 println!(
166 "{}",
167 serde_json::to_string_pretty(&report).expect("an agent listing serializes")
168 );
169 Ok(())
170}
171
172fn build_list_report(agents_dir: &Path, cwd: &Path, config: &Config) -> ListReport {
175 let installed = scan_directory_for_agents(agents_dir, config, cwd);
176 let local = read_agent_info(&cwd.join("agent.leviath"), config, cwd);
177 let configured: Vec<(PathBuf, AgentInfo)> = config
178 .agent_paths
179 .iter()
180 .flat_map(|dir| scan_directory_for_agents(dir, config, cwd))
181 .collect();
182
183 let from = |entries: Vec<(PathBuf, AgentInfo)>, source| {
184 entries.into_iter().map(move |(path, info)| ListedAgent {
185 info,
186 source,
187 path: path.display().to_string(),
188 })
189 };
190 let mut agents: Vec<ListedAgent> = from(installed, "installed")
191 .chain(from(configured, "configured"))
192 .collect();
193 if let Some(info) = local {
194 agents.push(ListedAgent {
195 info,
196 source: "local",
197 path: cwd.join("agent.leviath").display().to_string(),
198 });
199 }
200
201 ListReport {
202 agents,
203 bundled: crate::bundled::BUNDLED_AGENTS
204 .iter()
205 .map(|a| BundledEntry {
206 name: a.name.to_string(),
207 version: a.version.to_string(),
208 })
209 .collect(),
210 }
211}
212
213fn print_agent_listing(
217 agents_dir: &Path,
218 cwd: &Path,
219 exe_dir: Option<&Path>,
220 config: &Config,
221) -> anyhow::Result<()> {
222 let mut found_runnable = false;
228
229 let installed = scan_directory_for_agents(agents_dir, config, cwd);
231 if !installed.is_empty() {
232 found_runnable = true;
233 println!("Installed agents (~/.leviath/agents/):");
234 for (_path, info) in &installed {
235 print_agent(info);
236 }
237 println!();
238 }
239
240 let local_manifest = cwd.join("agent.leviath");
242 if local_manifest.exists()
243 && let Some(info) = read_agent_info(&local_manifest, config, cwd)
244 {
245 found_runnable = true;
246 println!("Local (current directory):");
247 print_agent(&info);
248 println!();
249 }
250
251 let mut config_agents = Vec::new();
253 for agent_path in &config.agent_paths {
254 let found = scan_directory_for_agents(agent_path, config, cwd);
255 config_agents.extend(found);
256 }
257 if !config_agents.is_empty() {
258 found_runnable = true;
259 println!("From configured paths:");
260 for (_path, info) in &config_agents {
261 print_agent(info);
262 }
263 println!();
264 }
265
266 let mut builtin_names: Vec<String> = crate::bundled::BUNDLED_AGENTS
274 .iter()
275 .map(|a| format!("{} (v{})", a.name, a.version))
276 .collect();
277 if let Some(exe_dir) = exe_dir {
278 for (_path, info) in scan_directory_for_agents(&exe_dir.join("agents"), config, cwd) {
279 let entry = format!("{} (v{})", info.name, info.version);
280 if !builtin_names.contains(&entry) {
281 builtin_names.push(entry);
282 }
283 }
284 }
285 println!("Bundled agents (install with `lev setup`):");
290 println!(" {}", builtin_names.join(", "));
291 println!();
292
293 if !found_runnable {
294 println!("No agents installed yet.");
295 println!();
296 println!("To install the bundled agents:");
297 println!(" lev setup");
298 println!();
299 println!("To create your own:");
300 println!(" lev create my-agent");
301 }
302
303 Ok(())
304}
305
306fn get_agents_dir_or_error(dir: Option<PathBuf>) -> anyhow::Result<PathBuf> {
310 dir.ok_or(anyhow::anyhow!("Could not determine home directory"))
311}
312
313fn get_agents_dir() -> anyhow::Result<PathBuf> {
328 #[cfg(test)]
329 if FORCE_AGENTS_DIR_ERROR.with(|f| f.get()) {
330 anyhow::bail!("Could not determine home directory");
331 }
332 get_agents_dir_or_error(leviath_core::paths::agents_dir())
333}
334
335#[cfg(test)]
336thread_local! {
337 static FORCE_AGENTS_DIR_ERROR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use crate::test_support::write_test_agent;
346
347 fn write_manifest(dir: &Path, name: &str) {
348 write_manifest_with_description(dir, name, "Test agent");
349 }
350
351 fn read_agent_info(manifest_path: &Path) -> Option<AgentInfo> {
355 super::read_agent_info(manifest_path, &Config::default(), Path::new("/work"))
356 }
357
358 fn scan_directory_for_agents(dir: &Path) -> Vec<(PathBuf, AgentInfo)> {
359 super::scan_directory_for_agents(dir, &Config::default(), Path::new("/work"))
360 }
361
362 fn write_manifest_with_description(dir: &Path, name: &str, description: &str) {
363 let content = format!(
364 r#"[agent]
365name = "{}"
366version = "1.0.0"
367description = "{}"
368
369[stages.main]
370mode = "autonomous"
371model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
372description = "Main"
373max_iterations = 5
374
375[context.regions]
376system = {{ kind = "pinned", max_tokens = 1000 }}
377"#,
378 name, description
379 );
380 write_test_agent(dir, content);
381 }
382
383 fn write_read_paths_manifest(dir: &Path, name: &str) {
386 let content = format!(
387 r#"[agent]
388name = "{name}"
389version = "1.0.0"
390description = "Test agent"
391
392[stages.main]
393mode = "autonomous"
394model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
395description = "Main"
396max_iterations = 5
397
398[context.regions]
399system = {{ kind = "pinned", max_tokens = 1000 }}
400
401[read_paths]
402allow = ["/data/runs"]
403"#
404 );
405 write_test_agent(dir, content);
406 }
407
408 fn info_with_config(dir: &Path, config: &Config) -> AgentInfo {
409 super::read_agent_info(&dir.join("agent.leviath"), config, Path::new("/work"))
410 .expect("manifest parses")
411 }
412
413 #[test]
416 fn an_ungranted_read_paths_declaration_is_listed_as_such() {
417 let dir = tempfile::tempdir().unwrap();
418 write_read_paths_manifest(dir.path(), "cto");
419 let summary = info_with_config(dir.path(), &Config::default())
420 .read_paths
421 .expect("declares read paths");
422 assert!(summary.contains("1 declared, 0 granted"), "{summary}");
423 assert!(summary.contains("lev validate"), "{summary}");
424 }
425
426 #[test]
427 fn a_granted_read_paths_declaration_needs_no_pointer() {
428 let dir = tempfile::tempdir().unwrap();
429 write_read_paths_manifest(dir.path(), "cto");
430 let mut config = Config::default();
431 config.security.read_paths = vec!["/data/runs".to_string()];
432 let summary = info_with_config(dir.path(), &config)
433 .read_paths
434 .expect("declares read paths");
435 assert_eq!(summary, "read_paths: 1 declared, 1 granted");
436 }
437
438 #[test]
441 fn a_broken_grant_list_is_reported_on_the_agent() {
442 let dir = tempfile::tempdir().unwrap();
443 write_read_paths_manifest(dir.path(), "cto");
444 let mut config = Config::default();
445 config.security.read_paths = vec!["regex:relative/.*".to_string()];
446 let summary = info_with_config(dir.path(), &config)
447 .read_paths
448 .expect("declares read paths");
449 assert!(summary.contains("config.toml"), "{summary}");
450 }
451
452 #[test]
453 fn an_agent_declaring_no_read_paths_gets_no_line() {
454 let dir = tempfile::tempdir().unwrap();
455 write_manifest(dir.path(), "plain");
456 assert!(
457 info_with_config(dir.path(), &Config::default())
458 .read_paths
459 .is_none()
460 );
461 }
462
463 #[test]
464 fn read_agent_info_valid_manifest() {
465 let dir = tempfile::tempdir().unwrap();
466 write_manifest(dir.path(), "my-agent");
467 let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
468 assert_eq!(info.name, "my-agent");
469 assert_eq!(info.version, "1.0.0");
470 assert_eq!(info.description, "Test agent");
471 }
472
473 #[test]
474 fn read_agent_info_missing_file_returns_none() {
475 let result = read_agent_info(Path::new("/nonexistent/agent.leviath"));
476 assert!(result.is_none());
477 }
478
479 #[test]
480 fn read_agent_info_invalid_toml_returns_none() {
481 let dir = tempfile::tempdir().unwrap();
482 fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
483 let result = read_agent_info(&dir.path().join("agent.leviath"));
484 assert!(result.is_none());
485 }
486
487 #[test]
488 fn scan_directory_nonexistent_returns_empty() {
489 let agents = scan_directory_for_agents(Path::new("/nonexistent/path"));
490 assert!(agents.is_empty());
491 }
492
493 #[test]
494 fn scan_directory_path_is_a_file_returns_empty() {
495 let tmp = tempfile::tempdir().unwrap();
500 let file_path = tmp.path().join("not-a-directory.txt");
501 fs::write(&file_path, "hello").unwrap();
502 let agents = scan_directory_for_agents(&file_path);
503 assert!(agents.is_empty());
504 }
505
506 #[test]
507 fn scan_directory_direct_manifest_invalid_is_skipped() {
508 let dir = tempfile::tempdir().unwrap();
514 fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
515 let agents = scan_directory_for_agents(dir.path());
516 assert!(agents.is_empty());
517 }
518
519 #[test]
520 fn scan_directory_with_direct_manifest() {
521 let dir = tempfile::tempdir().unwrap();
522 write_manifest(dir.path(), "direct-agent");
523 let agents = scan_directory_for_agents(dir.path());
524 assert_eq!(agents.len(), 1);
525 assert_eq!(agents[0].1.name, "direct-agent");
526 }
527
528 #[test]
529 fn scan_directory_with_subdirectories() {
530 let dir = tempfile::tempdir().unwrap();
531 let sub1 = dir.path().join("agent-a");
532 let sub2 = dir.path().join("agent-b");
533 fs::create_dir_all(&sub1).unwrap();
534 fs::create_dir_all(&sub2).unwrap();
535 write_manifest(&sub1, "agent-a");
536 write_manifest(&sub2, "agent-b");
537
538 let agents = scan_directory_for_agents(dir.path());
539 assert_eq!(agents.len(), 2);
540 let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
541 assert!(names.contains(&"agent-a"));
542 assert!(names.contains(&"agent-b"));
543 }
544
545 #[test]
546 fn scan_directory_ignores_subdirs_without_manifest() {
547 let dir = tempfile::tempdir().unwrap();
548 let sub = dir.path().join("no-manifest");
549 fs::create_dir_all(&sub).unwrap();
550 fs::write(sub.join("readme.txt"), "not a manifest").unwrap();
551
552 let agents = scan_directory_for_agents(dir.path());
553 assert!(agents.is_empty());
554 }
555
556 #[test]
557 fn list_args_default_filter() {
558 let args = ListArgs {
559 filter: "all".to_string(),
560 json: false,
561 };
562 assert_eq!(args.filter, "all");
563 }
564
565 #[test]
568 fn read_agent_info_extracts_description() {
569 let dir = tempfile::tempdir().unwrap();
570 write_manifest(dir.path(), "my-agent");
571 let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
572 assert_eq!(info.description, "Test agent");
573 assert_eq!(info.version, "1.0.0");
574 }
575
576 #[test]
579 fn scan_directory_with_both_direct_and_subdirs() {
580 let dir = tempfile::tempdir().unwrap();
581 write_manifest(dir.path(), "root-agent");
583 let sub = dir.path().join("child");
585 fs::create_dir_all(&sub).unwrap();
586 write_manifest(&sub, "child-agent");
587
588 let agents = scan_directory_for_agents(dir.path());
589 assert_eq!(agents.len(), 2);
590 let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
591 assert!(names.contains(&"root-agent"));
592 assert!(names.contains(&"child-agent"));
593 }
594
595 #[test]
598 fn scan_directory_empty_dir() {
599 let dir = tempfile::tempdir().unwrap();
600 let agents = scan_directory_for_agents(dir.path());
601 assert!(agents.is_empty());
602 }
603
604 #[test]
607 fn scan_directory_subdir_with_invalid_manifest() {
608 let dir = tempfile::tempdir().unwrap();
609 let sub = dir.path().join("bad-agent");
610 fs::create_dir_all(&sub).unwrap();
611 fs::write(sub.join("agent.leviath"), "invalid toml {{{{").unwrap();
612
613 let agents = scan_directory_for_agents(dir.path());
614 assert!(agents.is_empty());
615 }
616
617 #[test]
620 fn get_agents_dir_returns_path_with_agents() {
621 let dir = get_agents_dir().unwrap();
622 assert!(dir.to_str().unwrap().contains(".leviath"));
623 assert!(dir.to_str().unwrap().ends_with("agents"));
624 }
625
626 #[test]
627 fn get_agents_dir_or_error_some_returns_path() {
628 let dir = PathBuf::from("/home/testuser/.leviath/agents");
629 assert_eq!(get_agents_dir_or_error(Some(dir.clone())).unwrap(), dir);
630 }
631
632 #[test]
633 fn get_agents_dir_or_error_none_returns_error() {
634 let err = get_agents_dir_or_error(None).unwrap_err();
635 assert!(
636 err.to_string()
637 .contains("Could not determine home directory")
638 );
639 }
640
641 #[test]
644 fn read_agent_info_minimal_manifest() {
645 let dir = tempfile::tempdir().unwrap();
646 let content = r#"[agent]
647name = "minimal"
648version = "0.0.1"
649description = ""
650
651[stages.main]
652mode = "autonomous"
653model = { provider = "anthropic", model = "claude-sonnet-4-6" }
654description = "Main"
655max_iterations = 5
656
657[context.regions]
658system = { kind = "pinned", max_tokens = 1000 }
659"#;
660 write_test_agent(dir.path(), content);
661 let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
662 assert_eq!(info.name, "minimal");
663 assert_eq!(info.description, "");
664 }
665
666 #[tokio::test]
669 async fn execute_runs_without_error() {
670 crate::config::with_isolated_config_path_async("list-runs-ok", |_fake_dir| async move {
673 let args = ListArgs {
676 filter: "all".to_string(),
677 json: false,
678 };
679 let result = execute(args).await;
680 assert!(result.is_ok());
681 })
682 .await;
683 }
684
685 #[tokio::test]
686 async fn execute_returns_err_when_agents_dir_unresolvable() {
687 crate::config::with_isolated_config_path_async("list-dir-err", |_fake_dir| async move {
690 FORCE_AGENTS_DIR_ERROR.with(|f| f.set(true));
695 let args = ListArgs {
696 filter: "all".to_string(),
697 json: false,
698 };
699 let result = execute(args).await;
700 FORCE_AGENTS_DIR_ERROR.with(|f| f.set(false));
701
702 let err = result.unwrap_err();
703 assert!(
704 err.to_string()
705 .contains("Could not determine home directory")
706 );
707 })
708 .await;
709 }
710
711 #[cfg(unix)]
721 #[tokio::test]
722 async fn execute_falls_back_to_default_cwd_when_current_dir_is_gone() {
723 crate::config::with_isolated_config_path_async("list-cwd-gone", |_fake_dir| async move {
726 let _guard = crate::config::isolate_cwd_for_test();
730 let dir = std::env::temp_dir().join("lev-test-list-cwd-gone");
731 let _ = std::fs::remove_dir_all(&dir);
732 std::fs::create_dir_all(&dir).unwrap();
733 std::env::set_current_dir(&dir).unwrap();
734 std::fs::remove_dir_all(&dir).unwrap();
735
736 let args = ListArgs {
737 filter: "all".to_string(),
738 json: false,
739 };
740 let result = execute(args).await;
741
742 assert!(result.is_ok());
743 })
744 .await;
745 }
746
747 #[tokio::test]
753 async fn execute_falls_back_to_default_cwd_via_forced_error() {
754 crate::config::with_isolated_config_path_async("list-cwd-forced", |_fake_dir| async move {
757 crate::commands::force_cwd_error(true);
758 let args = ListArgs {
759 filter: "all".to_string(),
760 json: false,
761 };
762 let result = execute(args).await;
763 crate::commands::force_cwd_error(false);
764
765 assert!(result.is_ok());
766 })
767 .await;
768 }
769
770 #[tokio::test]
775 async fn execute_fails_loudly_on_a_broken_config() {
776 crate::config::with_isolated_config_path_async(
777 "list-broken-config",
778 |fake_dir| async move {
779 std::fs::write(fake_dir.join("config.toml"), "not = valid = toml").unwrap();
780 let args = ListArgs {
781 filter: "all".to_string(),
782 json: false,
783 };
784 let err = execute(args).await.expect_err("broken config must error");
785 assert!(err.to_string().contains("parse"), "{err}");
786 },
787 )
788 .await;
789 }
790
791 #[test]
796 fn json_listing_tags_each_agent_with_where_it_came_from() {
797 let agents_dir = tempfile::tempdir().unwrap();
798 let cwd = tempfile::tempdir().unwrap();
799 let configured = tempfile::tempdir().unwrap();
800
801 let installed = agents_dir.path().join("from-install");
802 fs::create_dir_all(&installed).unwrap();
803 write_manifest(&installed, "installed-agent");
804 write_manifest(cwd.path(), "local-agent");
805 let extra = configured.path().join("from-config");
806 fs::create_dir_all(&extra).unwrap();
807 write_manifest(&extra, "configured-agent");
808
809 let config = Config {
810 agent_paths: vec![configured.path().to_path_buf()],
811 ..Config::default()
812 };
813 let report = build_list_report(agents_dir.path(), cwd.path(), &config);
814
815 let sourced: Vec<(&str, &str)> = report
816 .agents
817 .iter()
818 .map(|a| (a.info.name.as_str(), a.source))
819 .collect();
820 assert!(sourced.contains(&("installed-agent", "installed")));
821 assert!(sourced.contains(&("configured-agent", "configured")));
822 assert!(sourced.contains(&("local-agent", "local")));
823 }
824
825 #[test]
826 fn json_listing_reports_the_bundled_catalog_separately_from_runnable_agents() {
827 let agents_dir = tempfile::tempdir().unwrap();
830 let cwd = tempfile::tempdir().unwrap();
831
832 let report = build_list_report(agents_dir.path(), cwd.path(), &Config::default());
833 assert!(report.agents.is_empty());
834 assert_eq!(report.bundled.len(), crate::bundled::BUNDLED_AGENTS.len());
835 }
836
837 #[test]
838 fn json_listing_flattens_the_agent_fields_next_to_its_source() {
839 let agents_dir = tempfile::tempdir().unwrap();
842 let cwd = tempfile::tempdir().unwrap();
843 write_manifest(cwd.path(), "flat-agent");
844
845 let report = build_list_report(agents_dir.path(), cwd.path(), &Config::default());
846 let value: serde_json::Value =
847 serde_json::from_str(&serde_json::to_string(&report).unwrap()).unwrap();
848 assert_eq!(value["agents"][0]["name"], serde_json::json!("flat-agent"));
849 assert_eq!(value["agents"][0]["source"], serde_json::json!("local"));
850 assert!(value["agents"][0]["path"].is_string());
851 }
852
853 #[tokio::test]
854 async fn execute_with_json_runs_without_error() {
855 crate::config::with_isolated_config_path_async("list-json-ok", |_fake_dir| async move {
856 let args = ListArgs {
857 filter: "all".to_string(),
858 json: true,
859 };
860 assert!(execute(args).await.is_ok());
861 })
862 .await;
863 }
864
865 #[test]
866 fn print_agent_listing_nothing_installed() {
867 let agents_dir = tempfile::tempdir().unwrap();
871 let cwd = tempfile::tempdir().unwrap();
872 let config = Config::default();
873
874 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
875 assert!(result.is_ok());
876 }
877
878 #[test]
879 fn print_agent_listing_finds_installed_agent() {
880 let agents_dir = tempfile::tempdir().unwrap();
881 let sub = agents_dir.path().join("installed-agent");
882 fs::create_dir_all(&sub).unwrap();
883 write_manifest(&sub, "installed-agent");
884
885 let cwd = tempfile::tempdir().unwrap();
886 let config = Config::default();
887
888 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
889 assert!(result.is_ok());
890 }
891
892 #[test]
893 fn print_agent_listing_finds_local_manifest() {
894 let agents_dir = tempfile::tempdir().unwrap();
895 let cwd = tempfile::tempdir().unwrap();
896 write_manifest(cwd.path(), "local-agent");
897 let config = Config::default();
898
899 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
900 assert!(result.is_ok());
901 }
902
903 #[test]
904 fn print_agent_listing_local_manifest_invalid_is_skipped() {
905 let agents_dir = tempfile::tempdir().unwrap();
911 let cwd = tempfile::tempdir().unwrap();
912 fs::write(cwd.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
913 let config = Config::default();
914
915 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
916 assert!(result.is_ok());
917 }
918
919 #[test]
920 fn print_agent_listing_finds_configured_path_agent() {
921 let agents_dir = tempfile::tempdir().unwrap();
922 let cwd = tempfile::tempdir().unwrap();
923 let configured = tempfile::tempdir().unwrap();
924 let sub = configured.path().join("configured-agent");
925 fs::create_dir_all(&sub).unwrap();
926 write_manifest(&sub, "configured-agent");
927
928 let config = Config {
929 agent_paths: vec![configured.path().to_path_buf()],
930 ..Config::default()
931 };
932
933 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
934 assert!(result.is_ok());
935 }
936
937 #[test]
938 fn print_agent_listing_finds_builtin_agents() {
939 let agents_dir = tempfile::tempdir().unwrap();
942 let cwd = tempfile::tempdir().unwrap();
943 let exe_dir = tempfile::tempdir().unwrap();
944 let builtin_dir = exe_dir.path().join("agents");
945 let sub = builtin_dir.join("builtin-agent");
946 fs::create_dir_all(&sub).unwrap();
947 write_manifest(&sub, "builtin-agent");
948 let config = Config::default();
949
950 let result =
951 print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
952 assert!(result.is_ok());
953 }
954
955 #[test]
958 fn print_agent_listing_carries_the_read_paths_line() {
959 let agents_dir = tempfile::tempdir().unwrap();
960 let agent = agents_dir.path().join("cto");
961 fs::create_dir_all(&agent).unwrap();
962 write_read_paths_manifest(&agent, "cto");
963 let cwd = tempfile::tempdir().unwrap();
964
965 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &Config::default());
966
967 assert!(result.is_ok());
968 assert!(
971 info_with_config(&agent, &Config::default())
972 .read_paths
973 .is_some()
974 );
975 }
976
977 #[test]
978 fn print_agent_listing_does_not_list_a_bundled_agent_twice() {
979 let bundled = &crate::bundled::BUNDLED_AGENTS[0];
984 let agents_dir = tempfile::tempdir().unwrap();
985 let cwd = tempfile::tempdir().unwrap();
986 let exe_dir = tempfile::tempdir().unwrap();
987 let sub = exe_dir.path().join("agents").join(bundled.name);
988 fs::create_dir_all(&sub).unwrap();
989 crate::bundled::install_bundled(bundled, &exe_dir.path().join("agents")).unwrap();
990 let config = Config::default();
991
992 let result =
993 print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
994
995 assert!(result.is_ok());
996 let entry = format!("{} (v{})", bundled.name, bundled.version);
999 let names: Vec<String> = crate::bundled::BUNDLED_AGENTS
1000 .iter()
1001 .map(|a| format!("{} (v{})", a.name, a.version))
1002 .collect();
1003 assert_eq!(names.iter().filter(|n| **n == entry).count(), 1);
1004 }
1005
1006 #[test]
1007 fn print_agent_listing_all_sources_populated() {
1008 let agents_dir = tempfile::tempdir().unwrap();
1009 fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
1010 write_manifest(&agents_dir.path().join("installed"), "installed");
1011
1012 let cwd = tempfile::tempdir().unwrap();
1013 write_manifest(cwd.path(), "local");
1014
1015 let configured = tempfile::tempdir().unwrap();
1016 fs::create_dir_all(configured.path().join("configured")).unwrap();
1017 write_manifest(&configured.path().join("configured"), "configured");
1018
1019 let exe_dir = tempfile::tempdir().unwrap();
1020 let builtin_sub = exe_dir.path().join("agents").join("builtin");
1021 fs::create_dir_all(&builtin_sub).unwrap();
1022 write_manifest(&builtin_sub, "builtin");
1023
1024 let config = Config {
1025 agent_paths: vec![configured.path().to_path_buf()],
1026 ..Config::default()
1027 };
1028
1029 let result =
1030 print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
1031 assert!(result.is_ok());
1032 }
1033
1034 #[test]
1035 fn print_agent_listing_empty_descriptions_across_all_sources() {
1036 let agents_dir = tempfile::tempdir().unwrap();
1041 fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
1042 write_manifest_with_description(&agents_dir.path().join("installed"), "installed", "");
1043
1044 let cwd = tempfile::tempdir().unwrap();
1045 write_manifest_with_description(cwd.path(), "local", "");
1046
1047 let configured = tempfile::tempdir().unwrap();
1048 fs::create_dir_all(configured.path().join("configured")).unwrap();
1049 write_manifest_with_description(&configured.path().join("configured"), "configured", "");
1050
1051 let config = Config {
1052 agent_paths: vec![configured.path().to_path_buf()],
1053 ..Config::default()
1054 };
1055
1056 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
1057 assert!(result.is_ok());
1058 }
1059
1060 #[test]
1063 fn scan_directory_agent_with_empty_description() {
1064 let dir = tempfile::tempdir().unwrap();
1065 let sub = dir.path().join("my-agent");
1066 fs::create_dir_all(&sub).unwrap();
1067 let content = r#"[agent]
1068name = "my-agent"
1069version = "2.0.0"
1070description = ""
1071
1072[stages.main]
1073mode = "autonomous"
1074model = { provider = "anthropic", model = "claude-sonnet-4-6" }
1075description = "Main"
1076max_iterations = 5
1077
1078[context.regions]
1079system = { kind = "pinned", max_tokens = 1000 }
1080"#;
1081 write_test_agent(sub, content);
1082
1083 let agents = scan_directory_for_agents(dir.path());
1084 assert_eq!(agents.len(), 1);
1085 assert_eq!(agents[0].1.description, "");
1086 }
1087
1088 #[test]
1091 fn scan_directory_mixed_valid_and_invalid() {
1092 let dir = tempfile::tempdir().unwrap();
1093 let good = dir.path().join("good");
1094 let bad = dir.path().join("bad");
1095 let empty = dir.path().join("empty");
1096 fs::create_dir_all(&good).unwrap();
1097 fs::create_dir_all(&bad).unwrap();
1098 fs::create_dir_all(&empty).unwrap();
1099
1100 write_manifest(&good, "good-agent");
1101 fs::write(bad.join("agent.leviath"), "bad {{ toml").unwrap();
1102
1103 let agents = scan_directory_for_agents(dir.path());
1104 assert_eq!(agents.len(), 1);
1105 assert_eq!(agents[0].1.name, "good-agent");
1106 }
1107}