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
18struct AgentInfo {
20 name: String,
21 version: String,
22 description: String,
23 read_paths: Option<String>,
28}
29
30fn read_agent_info(manifest_path: &Path, config: &Config, cwd: &Path) -> Option<AgentInfo> {
31 let content = fs::read_to_string(manifest_path).ok()?;
32 let blueprint = parse_manifest(&content).ok()?;
33 let read_paths = read_path_summary(&blueprint, config, cwd);
34 Some(AgentInfo {
35 name: blueprint.name,
36 version: blueprint.version,
37 description: blueprint.description,
38 read_paths,
39 })
40}
41
42fn read_path_summary(
46 blueprint: &leviath_core::Blueprint,
47 config: &Config,
48 cwd: &Path,
49) -> Option<String> {
50 match crate::read_path_report::build(blueprint, config, cwd)? {
51 Ok(report) if report.has_ungranted() => Some(format!(
52 "read_paths: {} - `lev validate` shows which",
53 report.summary()
54 )),
55 Ok(report) => Some(format!("read_paths: {}", report.summary())),
56 Err(e) => Some(format!("read_paths: {e}")),
57 }
58}
59
60fn scan_directory_for_agents(dir: &Path, config: &Config, cwd: &Path) -> Vec<(PathBuf, AgentInfo)> {
61 let mut agents = Vec::new();
62 if !dir.exists() {
63 return agents;
64 }
65
66 let direct_manifest = dir.join("agent.leviath");
68 if direct_manifest.exists()
69 && let Some(info) = read_agent_info(&direct_manifest, config, cwd)
70 {
71 agents.push((dir.to_path_buf(), info));
72 }
73
74 if let Ok(entries) = fs::read_dir(dir) {
76 for entry in entries.flatten() {
77 let path = entry.path();
78 if path.is_dir() {
79 let manifest_path = path.join("agent.leviath");
80 if manifest_path.exists()
81 && let Some(info) = read_agent_info(&manifest_path, config, cwd)
82 {
83 agents.push((path, info));
84 }
85 }
86 }
87 }
88
89 agents
90}
91
92fn print_agent(info: &AgentInfo) {
95 let desc = if info.description.is_empty() {
96 String::new()
97 } else {
98 format!(" - {}", info.description)
99 };
100 println!(" {} (v{}){}", info.name, info.version, desc);
101 if let Some(read_paths) = &info.read_paths {
102 println!(" {read_paths}");
103 }
104}
105
106pub async fn execute(_args: ListArgs) -> anyhow::Result<()> {
107 let config = Config::load()?;
111 let agents_dir = get_agents_dir()?;
112 let cwd = resolve_cwd().unwrap_or_default();
113 let exe_dir = std::env::current_exe()
114 .ok()
115 .and_then(|p| p.parent().map(|p| p.to_path_buf()));
116
117 print_agent_listing(&agents_dir, &cwd, exe_dir.as_deref(), &config)
118}
119
120fn print_agent_listing(
124 agents_dir: &Path,
125 cwd: &Path,
126 exe_dir: Option<&Path>,
127 config: &Config,
128) -> anyhow::Result<()> {
129 let mut found_runnable = false;
135
136 let installed = scan_directory_for_agents(agents_dir, config, cwd);
138 if !installed.is_empty() {
139 found_runnable = true;
140 println!("Installed agents (~/.leviath/agents/):");
141 for (_path, info) in &installed {
142 print_agent(info);
143 }
144 println!();
145 }
146
147 let local_manifest = cwd.join("agent.leviath");
149 if local_manifest.exists()
150 && let Some(info) = read_agent_info(&local_manifest, config, cwd)
151 {
152 found_runnable = true;
153 println!("Local (current directory):");
154 print_agent(&info);
155 println!();
156 }
157
158 let mut config_agents = Vec::new();
160 for agent_path in &config.agent_paths {
161 let found = scan_directory_for_agents(agent_path, config, cwd);
162 config_agents.extend(found);
163 }
164 if !config_agents.is_empty() {
165 found_runnable = true;
166 println!("From configured paths:");
167 for (_path, info) in &config_agents {
168 print_agent(info);
169 }
170 println!();
171 }
172
173 let mut builtin_names: Vec<String> = crate::bundled::BUNDLED_AGENTS
181 .iter()
182 .map(|a| format!("{} (v{})", a.name, a.version))
183 .collect();
184 if let Some(exe_dir) = exe_dir {
185 for (_path, info) in scan_directory_for_agents(&exe_dir.join("agents"), config, cwd) {
186 let entry = format!("{} (v{})", info.name, info.version);
187 if !builtin_names.contains(&entry) {
188 builtin_names.push(entry);
189 }
190 }
191 }
192 println!("Bundled agents (install with `lev setup`):");
197 println!(" {}", builtin_names.join(", "));
198 println!();
199
200 if !found_runnable {
201 println!("No agents installed yet.");
202 println!();
203 println!("To install the bundled agents:");
204 println!(" lev setup");
205 println!();
206 println!("To create your own:");
207 println!(" lev create my-agent");
208 }
209
210 Ok(())
211}
212
213fn get_agents_dir_or_error(dir: Option<PathBuf>) -> anyhow::Result<PathBuf> {
217 dir.ok_or(anyhow::anyhow!("Could not determine home directory"))
218}
219
220fn get_agents_dir() -> anyhow::Result<PathBuf> {
235 #[cfg(test)]
236 if FORCE_AGENTS_DIR_ERROR.with(|f| f.get()) {
237 anyhow::bail!("Could not determine home directory");
238 }
239 get_agents_dir_or_error(leviath_core::paths::agents_dir())
240}
241
242#[cfg(test)]
243thread_local! {
244 static FORCE_AGENTS_DIR_ERROR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::test_support::write_test_agent;
253
254 fn write_manifest(dir: &Path, name: &str) {
255 write_manifest_with_description(dir, name, "Test agent");
256 }
257
258 fn read_agent_info(manifest_path: &Path) -> Option<AgentInfo> {
262 super::read_agent_info(manifest_path, &Config::default(), Path::new("/work"))
263 }
264
265 fn scan_directory_for_agents(dir: &Path) -> Vec<(PathBuf, AgentInfo)> {
266 super::scan_directory_for_agents(dir, &Config::default(), Path::new("/work"))
267 }
268
269 fn write_manifest_with_description(dir: &Path, name: &str, description: &str) {
270 let content = format!(
271 r#"[agent]
272name = "{}"
273version = "1.0.0"
274description = "{}"
275
276[stages.main]
277mode = "autonomous"
278model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
279description = "Main"
280max_iterations = 5
281
282[context.regions]
283system = {{ kind = "pinned", max_tokens = 1000 }}
284"#,
285 name, description
286 );
287 write_test_agent(dir, content);
288 }
289
290 fn write_read_paths_manifest(dir: &Path, name: &str) {
293 let content = format!(
294 r#"[agent]
295name = "{name}"
296version = "1.0.0"
297description = "Test agent"
298
299[stages.main]
300mode = "autonomous"
301model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
302description = "Main"
303max_iterations = 5
304
305[context.regions]
306system = {{ kind = "pinned", max_tokens = 1000 }}
307
308[read_paths]
309allow = ["/data/runs"]
310"#
311 );
312 write_test_agent(dir, content);
313 }
314
315 fn info_with_config(dir: &Path, config: &Config) -> AgentInfo {
316 super::read_agent_info(&dir.join("agent.leviath"), config, Path::new("/work"))
317 .expect("manifest parses")
318 }
319
320 #[test]
323 fn an_ungranted_read_paths_declaration_is_listed_as_such() {
324 let dir = tempfile::tempdir().unwrap();
325 write_read_paths_manifest(dir.path(), "cto");
326 let summary = info_with_config(dir.path(), &Config::default())
327 .read_paths
328 .expect("declares read paths");
329 assert!(summary.contains("1 declared, 0 granted"), "{summary}");
330 assert!(summary.contains("lev validate"), "{summary}");
331 }
332
333 #[test]
334 fn a_granted_read_paths_declaration_needs_no_pointer() {
335 let dir = tempfile::tempdir().unwrap();
336 write_read_paths_manifest(dir.path(), "cto");
337 let mut config = Config::default();
338 config.security.read_paths = vec!["/data/runs".to_string()];
339 let summary = info_with_config(dir.path(), &config)
340 .read_paths
341 .expect("declares read paths");
342 assert_eq!(summary, "read_paths: 1 declared, 1 granted");
343 }
344
345 #[test]
348 fn a_broken_grant_list_is_reported_on_the_agent() {
349 let dir = tempfile::tempdir().unwrap();
350 write_read_paths_manifest(dir.path(), "cto");
351 let mut config = Config::default();
352 config.security.read_paths = vec!["regex:relative/.*".to_string()];
353 let summary = info_with_config(dir.path(), &config)
354 .read_paths
355 .expect("declares read paths");
356 assert!(summary.contains("config.toml"), "{summary}");
357 }
358
359 #[test]
360 fn an_agent_declaring_no_read_paths_gets_no_line() {
361 let dir = tempfile::tempdir().unwrap();
362 write_manifest(dir.path(), "plain");
363 assert!(
364 info_with_config(dir.path(), &Config::default())
365 .read_paths
366 .is_none()
367 );
368 }
369
370 #[test]
371 fn read_agent_info_valid_manifest() {
372 let dir = tempfile::tempdir().unwrap();
373 write_manifest(dir.path(), "my-agent");
374 let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
375 assert_eq!(info.name, "my-agent");
376 assert_eq!(info.version, "1.0.0");
377 assert_eq!(info.description, "Test agent");
378 }
379
380 #[test]
381 fn read_agent_info_missing_file_returns_none() {
382 let result = read_agent_info(Path::new("/nonexistent/agent.leviath"));
383 assert!(result.is_none());
384 }
385
386 #[test]
387 fn read_agent_info_invalid_toml_returns_none() {
388 let dir = tempfile::tempdir().unwrap();
389 fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
390 let result = read_agent_info(&dir.path().join("agent.leviath"));
391 assert!(result.is_none());
392 }
393
394 #[test]
395 fn scan_directory_nonexistent_returns_empty() {
396 let agents = scan_directory_for_agents(Path::new("/nonexistent/path"));
397 assert!(agents.is_empty());
398 }
399
400 #[test]
401 fn scan_directory_path_is_a_file_returns_empty() {
402 let tmp = tempfile::tempdir().unwrap();
407 let file_path = tmp.path().join("not-a-directory.txt");
408 fs::write(&file_path, "hello").unwrap();
409 let agents = scan_directory_for_agents(&file_path);
410 assert!(agents.is_empty());
411 }
412
413 #[test]
414 fn scan_directory_direct_manifest_invalid_is_skipped() {
415 let dir = tempfile::tempdir().unwrap();
421 fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
422 let agents = scan_directory_for_agents(dir.path());
423 assert!(agents.is_empty());
424 }
425
426 #[test]
427 fn scan_directory_with_direct_manifest() {
428 let dir = tempfile::tempdir().unwrap();
429 write_manifest(dir.path(), "direct-agent");
430 let agents = scan_directory_for_agents(dir.path());
431 assert_eq!(agents.len(), 1);
432 assert_eq!(agents[0].1.name, "direct-agent");
433 }
434
435 #[test]
436 fn scan_directory_with_subdirectories() {
437 let dir = tempfile::tempdir().unwrap();
438 let sub1 = dir.path().join("agent-a");
439 let sub2 = dir.path().join("agent-b");
440 fs::create_dir_all(&sub1).unwrap();
441 fs::create_dir_all(&sub2).unwrap();
442 write_manifest(&sub1, "agent-a");
443 write_manifest(&sub2, "agent-b");
444
445 let agents = scan_directory_for_agents(dir.path());
446 assert_eq!(agents.len(), 2);
447 let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
448 assert!(names.contains(&"agent-a"));
449 assert!(names.contains(&"agent-b"));
450 }
451
452 #[test]
453 fn scan_directory_ignores_subdirs_without_manifest() {
454 let dir = tempfile::tempdir().unwrap();
455 let sub = dir.path().join("no-manifest");
456 fs::create_dir_all(&sub).unwrap();
457 fs::write(sub.join("readme.txt"), "not a manifest").unwrap();
458
459 let agents = scan_directory_for_agents(dir.path());
460 assert!(agents.is_empty());
461 }
462
463 #[test]
464 fn list_args_default_filter() {
465 let args = ListArgs {
466 filter: "all".to_string(),
467 };
468 assert_eq!(args.filter, "all");
469 }
470
471 #[test]
474 fn read_agent_info_extracts_description() {
475 let dir = tempfile::tempdir().unwrap();
476 write_manifest(dir.path(), "my-agent");
477 let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
478 assert_eq!(info.description, "Test agent");
479 assert_eq!(info.version, "1.0.0");
480 }
481
482 #[test]
485 fn scan_directory_with_both_direct_and_subdirs() {
486 let dir = tempfile::tempdir().unwrap();
487 write_manifest(dir.path(), "root-agent");
489 let sub = dir.path().join("child");
491 fs::create_dir_all(&sub).unwrap();
492 write_manifest(&sub, "child-agent");
493
494 let agents = scan_directory_for_agents(dir.path());
495 assert_eq!(agents.len(), 2);
496 let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
497 assert!(names.contains(&"root-agent"));
498 assert!(names.contains(&"child-agent"));
499 }
500
501 #[test]
504 fn scan_directory_empty_dir() {
505 let dir = tempfile::tempdir().unwrap();
506 let agents = scan_directory_for_agents(dir.path());
507 assert!(agents.is_empty());
508 }
509
510 #[test]
513 fn scan_directory_subdir_with_invalid_manifest() {
514 let dir = tempfile::tempdir().unwrap();
515 let sub = dir.path().join("bad-agent");
516 fs::create_dir_all(&sub).unwrap();
517 fs::write(sub.join("agent.leviath"), "invalid toml {{{{").unwrap();
518
519 let agents = scan_directory_for_agents(dir.path());
520 assert!(agents.is_empty());
521 }
522
523 #[test]
526 fn get_agents_dir_returns_path_with_agents() {
527 let dir = get_agents_dir().unwrap();
528 assert!(dir.to_str().unwrap().contains(".leviath"));
529 assert!(dir.to_str().unwrap().ends_with("agents"));
530 }
531
532 #[test]
533 fn get_agents_dir_or_error_some_returns_path() {
534 let dir = PathBuf::from("/home/testuser/.leviath/agents");
535 assert_eq!(get_agents_dir_or_error(Some(dir.clone())).unwrap(), dir);
536 }
537
538 #[test]
539 fn get_agents_dir_or_error_none_returns_error() {
540 let err = get_agents_dir_or_error(None).unwrap_err();
541 assert!(
542 err.to_string()
543 .contains("Could not determine home directory")
544 );
545 }
546
547 #[test]
550 fn read_agent_info_minimal_manifest() {
551 let dir = tempfile::tempdir().unwrap();
552 let content = r#"[agent]
553name = "minimal"
554version = "0.0.1"
555description = ""
556
557[stages.main]
558mode = "autonomous"
559model = { provider = "anthropic", model = "claude-sonnet-4-6" }
560description = "Main"
561max_iterations = 5
562
563[context.regions]
564system = { kind = "pinned", max_tokens = 1000 }
565"#;
566 write_test_agent(dir.path(), content);
567 let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
568 assert_eq!(info.name, "minimal");
569 assert_eq!(info.description, "");
570 }
571
572 #[tokio::test]
575 async fn execute_runs_without_error() {
576 crate::config::with_isolated_config_path_async("list-runs-ok", |_fake_dir| async move {
579 let args = ListArgs {
582 filter: "all".to_string(),
583 };
584 let result = execute(args).await;
585 assert!(result.is_ok());
586 })
587 .await;
588 }
589
590 #[tokio::test]
591 async fn execute_returns_err_when_agents_dir_unresolvable() {
592 crate::config::with_isolated_config_path_async("list-dir-err", |_fake_dir| async move {
595 FORCE_AGENTS_DIR_ERROR.with(|f| f.set(true));
600 let args = ListArgs {
601 filter: "all".to_string(),
602 };
603 let result = execute(args).await;
604 FORCE_AGENTS_DIR_ERROR.with(|f| f.set(false));
605
606 let err = result.unwrap_err();
607 assert!(
608 err.to_string()
609 .contains("Could not determine home directory")
610 );
611 })
612 .await;
613 }
614
615 #[cfg(unix)]
625 #[tokio::test]
626 async fn execute_falls_back_to_default_cwd_when_current_dir_is_gone() {
627 crate::config::with_isolated_config_path_async("list-cwd-gone", |_fake_dir| async move {
630 let _guard = crate::config::isolate_cwd_for_test();
634 let dir = std::env::temp_dir().join("lev-test-list-cwd-gone");
635 let _ = std::fs::remove_dir_all(&dir);
636 std::fs::create_dir_all(&dir).unwrap();
637 std::env::set_current_dir(&dir).unwrap();
638 std::fs::remove_dir_all(&dir).unwrap();
639
640 let args = ListArgs {
641 filter: "all".to_string(),
642 };
643 let result = execute(args).await;
644
645 assert!(result.is_ok());
646 })
647 .await;
648 }
649
650 #[tokio::test]
656 async fn execute_falls_back_to_default_cwd_via_forced_error() {
657 crate::config::with_isolated_config_path_async("list-cwd-forced", |_fake_dir| async move {
660 crate::commands::force_cwd_error(true);
661 let args = ListArgs {
662 filter: "all".to_string(),
663 };
664 let result = execute(args).await;
665 crate::commands::force_cwd_error(false);
666
667 assert!(result.is_ok());
668 })
669 .await;
670 }
671
672 #[tokio::test]
677 async fn execute_fails_loudly_on_a_broken_config() {
678 crate::config::with_isolated_config_path_async(
679 "list-broken-config",
680 |fake_dir| async move {
681 std::fs::write(fake_dir.join("config.toml"), "not = valid = toml").unwrap();
682 let args = ListArgs {
683 filter: "all".to_string(),
684 };
685 let err = execute(args).await.expect_err("broken config must error");
686 assert!(err.to_string().contains("parse"), "{err}");
687 },
688 )
689 .await;
690 }
691
692 #[test]
695 fn print_agent_listing_nothing_installed() {
696 let agents_dir = tempfile::tempdir().unwrap();
700 let cwd = tempfile::tempdir().unwrap();
701 let config = Config::default();
702
703 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
704 assert!(result.is_ok());
705 }
706
707 #[test]
708 fn print_agent_listing_finds_installed_agent() {
709 let agents_dir = tempfile::tempdir().unwrap();
710 let sub = agents_dir.path().join("installed-agent");
711 fs::create_dir_all(&sub).unwrap();
712 write_manifest(&sub, "installed-agent");
713
714 let cwd = tempfile::tempdir().unwrap();
715 let config = Config::default();
716
717 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
718 assert!(result.is_ok());
719 }
720
721 #[test]
722 fn print_agent_listing_finds_local_manifest() {
723 let agents_dir = tempfile::tempdir().unwrap();
724 let cwd = tempfile::tempdir().unwrap();
725 write_manifest(cwd.path(), "local-agent");
726 let config = Config::default();
727
728 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
729 assert!(result.is_ok());
730 }
731
732 #[test]
733 fn print_agent_listing_local_manifest_invalid_is_skipped() {
734 let agents_dir = tempfile::tempdir().unwrap();
740 let cwd = tempfile::tempdir().unwrap();
741 fs::write(cwd.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
742 let config = Config::default();
743
744 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
745 assert!(result.is_ok());
746 }
747
748 #[test]
749 fn print_agent_listing_finds_configured_path_agent() {
750 let agents_dir = tempfile::tempdir().unwrap();
751 let cwd = tempfile::tempdir().unwrap();
752 let configured = tempfile::tempdir().unwrap();
753 let sub = configured.path().join("configured-agent");
754 fs::create_dir_all(&sub).unwrap();
755 write_manifest(&sub, "configured-agent");
756
757 let config = Config {
758 agent_paths: vec![configured.path().to_path_buf()],
759 ..Config::default()
760 };
761
762 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
763 assert!(result.is_ok());
764 }
765
766 #[test]
767 fn print_agent_listing_finds_builtin_agents() {
768 let agents_dir = tempfile::tempdir().unwrap();
771 let cwd = tempfile::tempdir().unwrap();
772 let exe_dir = tempfile::tempdir().unwrap();
773 let builtin_dir = exe_dir.path().join("agents");
774 let sub = builtin_dir.join("builtin-agent");
775 fs::create_dir_all(&sub).unwrap();
776 write_manifest(&sub, "builtin-agent");
777 let config = Config::default();
778
779 let result =
780 print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
781 assert!(result.is_ok());
782 }
783
784 #[test]
787 fn print_agent_listing_carries_the_read_paths_line() {
788 let agents_dir = tempfile::tempdir().unwrap();
789 let agent = agents_dir.path().join("cto");
790 fs::create_dir_all(&agent).unwrap();
791 write_read_paths_manifest(&agent, "cto");
792 let cwd = tempfile::tempdir().unwrap();
793
794 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &Config::default());
795
796 assert!(result.is_ok());
797 assert!(
800 info_with_config(&agent, &Config::default())
801 .read_paths
802 .is_some()
803 );
804 }
805
806 #[test]
807 fn print_agent_listing_does_not_list_a_bundled_agent_twice() {
808 let bundled = &crate::bundled::BUNDLED_AGENTS[0];
813 let agents_dir = tempfile::tempdir().unwrap();
814 let cwd = tempfile::tempdir().unwrap();
815 let exe_dir = tempfile::tempdir().unwrap();
816 let sub = exe_dir.path().join("agents").join(bundled.name);
817 fs::create_dir_all(&sub).unwrap();
818 crate::bundled::install_bundled(bundled, &exe_dir.path().join("agents")).unwrap();
819 let config = Config::default();
820
821 let result =
822 print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
823
824 assert!(result.is_ok());
825 let entry = format!("{} (v{})", bundled.name, bundled.version);
828 let names: Vec<String> = crate::bundled::BUNDLED_AGENTS
829 .iter()
830 .map(|a| format!("{} (v{})", a.name, a.version))
831 .collect();
832 assert_eq!(names.iter().filter(|n| **n == entry).count(), 1);
833 }
834
835 #[test]
836 fn print_agent_listing_all_sources_populated() {
837 let agents_dir = tempfile::tempdir().unwrap();
838 fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
839 write_manifest(&agents_dir.path().join("installed"), "installed");
840
841 let cwd = tempfile::tempdir().unwrap();
842 write_manifest(cwd.path(), "local");
843
844 let configured = tempfile::tempdir().unwrap();
845 fs::create_dir_all(configured.path().join("configured")).unwrap();
846 write_manifest(&configured.path().join("configured"), "configured");
847
848 let exe_dir = tempfile::tempdir().unwrap();
849 let builtin_sub = exe_dir.path().join("agents").join("builtin");
850 fs::create_dir_all(&builtin_sub).unwrap();
851 write_manifest(&builtin_sub, "builtin");
852
853 let config = Config {
854 agent_paths: vec![configured.path().to_path_buf()],
855 ..Config::default()
856 };
857
858 let result =
859 print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
860 assert!(result.is_ok());
861 }
862
863 #[test]
864 fn print_agent_listing_empty_descriptions_across_all_sources() {
865 let agents_dir = tempfile::tempdir().unwrap();
870 fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
871 write_manifest_with_description(&agents_dir.path().join("installed"), "installed", "");
872
873 let cwd = tempfile::tempdir().unwrap();
874 write_manifest_with_description(cwd.path(), "local", "");
875
876 let configured = tempfile::tempdir().unwrap();
877 fs::create_dir_all(configured.path().join("configured")).unwrap();
878 write_manifest_with_description(&configured.path().join("configured"), "configured", "");
879
880 let config = Config {
881 agent_paths: vec![configured.path().to_path_buf()],
882 ..Config::default()
883 };
884
885 let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
886 assert!(result.is_ok());
887 }
888
889 #[test]
892 fn scan_directory_agent_with_empty_description() {
893 let dir = tempfile::tempdir().unwrap();
894 let sub = dir.path().join("my-agent");
895 fs::create_dir_all(&sub).unwrap();
896 let content = r#"[agent]
897name = "my-agent"
898version = "2.0.0"
899description = ""
900
901[stages.main]
902mode = "autonomous"
903model = { provider = "anthropic", model = "claude-sonnet-4-6" }
904description = "Main"
905max_iterations = 5
906
907[context.regions]
908system = { kind = "pinned", max_tokens = 1000 }
909"#;
910 write_test_agent(sub, content);
911
912 let agents = scan_directory_for_agents(dir.path());
913 assert_eq!(agents.len(), 1);
914 assert_eq!(agents[0].1.description, "");
915 }
916
917 #[test]
920 fn scan_directory_mixed_valid_and_invalid() {
921 let dir = tempfile::tempdir().unwrap();
922 let good = dir.path().join("good");
923 let bad = dir.path().join("bad");
924 let empty = dir.path().join("empty");
925 fs::create_dir_all(&good).unwrap();
926 fs::create_dir_all(&bad).unwrap();
927 fs::create_dir_all(&empty).unwrap();
928
929 write_manifest(&good, "good-agent");
930 fs::write(bad.join("agent.leviath"), "bad {{ toml").unwrap();
931
932 let agents = scan_directory_for_agents(dir.path());
933 assert_eq!(agents.len(), 1);
934 assert_eq!(agents[0].1.name, "good-agent");
935 }
936}