Skip to main content

leviath_cli/commands/
list.rs

1//! `lev list` - List available agents and blueprints
2
3use clap::Args;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::config::Config;
8use leviath_core::manifest::parse_manifest;
9
10#[derive(Args)]
11pub struct ListArgs {
12    /// Filter by type (agents, blueprints, all)
13    #[arg(short, long, default_value = "all")]
14    pub filter: String,
15}
16
17/// Info parsed from an agent manifest for display.
18struct AgentInfo {
19    name: String,
20    version: String,
21    description: String,
22}
23
24fn read_agent_info(manifest_path: &Path) -> Option<AgentInfo> {
25    let content = fs::read_to_string(manifest_path).ok()?;
26    let blueprint = parse_manifest(&content).ok()?;
27    Some(AgentInfo {
28        name: blueprint.name,
29        version: blueprint.version,
30        description: blueprint.description,
31    })
32}
33
34fn scan_directory_for_agents(dir: &Path) -> Vec<(PathBuf, AgentInfo)> {
35    let mut agents = Vec::new();
36    if !dir.exists() {
37        return agents;
38    }
39
40    // Check if this directory itself has an agent.leviath
41    let direct_manifest = dir.join("agent.leviath");
42    if direct_manifest.exists()
43        && let Some(info) = read_agent_info(&direct_manifest)
44    {
45        agents.push((dir.to_path_buf(), info));
46    }
47
48    // Check subdirectories
49    if let Ok(entries) = fs::read_dir(dir) {
50        for entry in entries.flatten() {
51            let path = entry.path();
52            if path.is_dir() {
53                let manifest_path = path.join("agent.leviath");
54                if manifest_path.exists()
55                    && let Some(info) = read_agent_info(&manifest_path)
56                {
57                    agents.push((path, info));
58                }
59            }
60        }
61    }
62
63    agents
64}
65
66#[cfg(test)]
67thread_local! {
68    /// Test-only toggle letting `execute_falls_back_to_default_cwd_via_forced_error`
69    /// force [`resolve_cwd`]'s `Err` arm deterministically on every platform,
70    /// as a companion to
71    /// `execute_falls_back_to_default_cwd_when_current_dir_is_gone`'s genuine
72    /// Unix-only filesystem reproduction (real `remove_dir_all` of the live
73    /// CWD is a sharing violation on Windows, not a success, so that same
74    /// trick isn't available there).
75    static FORCE_CWD_ERROR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
76}
77
78/// Real CWD lookup, with a test-only failure-injection toggle so its `Err`
79/// arm can be forced deterministically (see [`FORCE_CWD_ERROR`]) without
80/// changing what production actually calls.
81fn resolve_cwd() -> std::io::Result<PathBuf> {
82    #[cfg(test)]
83    if FORCE_CWD_ERROR.with(|f| f.get()) {
84        return Err(std::io::Error::other("forced CWD error for testing"));
85    }
86    std::env::current_dir()
87}
88
89pub async fn execute(_args: ListArgs) -> anyhow::Result<()> {
90    // Propagate, don't default: a config that exists but doesn't parse would
91    // silently list from the default `agent_paths`, hiding the user's own
92    // agent directories with no hint why (a missing file loads as defaults).
93    let config = Config::load()?;
94    let agents_dir = get_agents_dir()?;
95    let cwd = resolve_cwd().unwrap_or_default();
96    let exe_dir = std::env::current_exe()
97        .ok()
98        .and_then(|p| p.parent().map(|p| p.to_path_buf()));
99
100    print_agent_listing(&agents_dir, &cwd, exe_dir.as_deref(), &config)
101}
102
103/// Core `lev list` logic, parameterized by every real-environment source it
104/// reads from so it can be tested against tempdirs instead of the real
105/// home directory / CWD / executable location / config.
106fn print_agent_listing(
107    agents_dir: &Path,
108    cwd: &Path,
109    exe_dir: Option<&Path>,
110    config: &Config,
111) -> anyhow::Result<()> {
112    // Tracks whether the user has any agent they can actually *run*. The
113    // bundled catalog deliberately does not count: it is always non-empty, and
114    // treating it as "you have agents" would suppress the get-started guidance
115    // for exactly the person who needs it - someone with a fresh install and
116    // nothing installed yet.
117    let mut found_runnable = false;
118
119    // 1. Installed agents (~/.leviath/agents/)
120    let installed = scan_directory_for_agents(agents_dir);
121    if !installed.is_empty() {
122        found_runnable = true;
123        println!("Installed agents (~/.leviath/agents/):");
124        for (_path, info) in &installed {
125            let desc = if info.description.is_empty() {
126                String::new()
127            } else {
128                format!(" - {}", info.description)
129            };
130            println!("  {} (v{}){}", info.name, info.version, desc);
131        }
132        println!();
133    }
134
135    // 2. Local (current directory)
136    let local_manifest = cwd.join("agent.leviath");
137    if local_manifest.exists()
138        && let Some(info) = read_agent_info(&local_manifest)
139    {
140        found_runnable = true;
141        let desc = if info.description.is_empty() {
142            String::new()
143        } else {
144            format!(" - {}", info.description)
145        };
146        println!("Local (current directory):");
147        println!("  {} (v{}){}", info.name, info.version, desc);
148        println!();
149    }
150
151    // 3. Config's agent_paths directories
152    let mut config_agents = Vec::new();
153    for agent_path in &config.agent_paths {
154        let found = scan_directory_for_agents(agent_path);
155        config_agents.extend(found);
156    }
157    if !config_agents.is_empty() {
158        found_runnable = true;
159        println!("From configured paths:");
160        for (_path, info) in &config_agents {
161            let desc = if info.description.is_empty() {
162                String::new()
163            } else {
164                format!(" - {}", info.description)
165            };
166            println!("  {} (v{}){}", info.name, info.version, desc);
167        }
168        println!();
169    }
170
171    // 4. Bundled agents - the blueprints embedded in this binary.
172    //
173    // Reports the embedded catalog, which is what `lev setup` installs from.
174    // Scanning only `<exe_dir>/agents` would leave this section blank outside a
175    // git checkout - a directory no real install has. The on-disk scan stays as
176    // a second source so a checkout or a packaging layout that *does* ship an
177    // `agents/` dir next to the binary still shows up.
178    let mut builtin_names: Vec<String> = crate::bundled::BUNDLED_AGENTS
179        .iter()
180        .map(|a| format!("{} (v{})", a.name, a.version))
181        .collect();
182    if let Some(exe_dir) = exe_dir {
183        for (_path, info) in scan_directory_for_agents(&exe_dir.join("agents")) {
184            let entry = format!("{} (v{})", info.name, info.version);
185            if !builtin_names.contains(&entry) {
186                builtin_names.push(entry);
187            }
188        }
189    }
190    // No emptiness guard: the embedded catalog is always populated (a build
191    // that found no blueprints fails `bundled`'s own invariant test), so an
192    // `if !builtin_names.is_empty()` here would be a branch that can never be
193    // false - unreachable code dressed up as a handled case.
194    println!("Bundled agents (install with `lev setup`):");
195    println!("  {}", builtin_names.join(", "));
196    println!();
197
198    if !found_runnable {
199        println!("No agents installed yet.");
200        println!();
201        println!("To install the bundled agents:");
202        println!("  lev setup");
203        println!();
204        println!("To create your own:");
205        println!("  lev create my-agent");
206    }
207
208    Ok(())
209}
210
211/// Core `get_agents_dir` logic, parameterized by the home directory so the
212/// "could not determine home directory" error path can be unit tested
213/// without depending on the real environment.
214fn get_agents_dir_or_error(dir: Option<PathBuf>) -> anyhow::Result<PathBuf> {
215    dir.ok_or(anyhow::anyhow!("Could not determine home directory"))
216}
217
218/// Resolve `~/.leviath/agents`, the directory `lev list` scans for installed
219/// agents.
220///
221/// A thin wrapper over [`get_agents_dir_or_error`] supplying the real
222/// resolved directory. The `#[cfg(test)]` guard below only lets tests force the
223/// "no home directory" error arm of `execute()` deterministically - the real
224/// the shared resolver can't be made to return `None` in any environment a
225/// test may safely create (on macOS `dirs::home_dir()` falls back to a
226/// passwd-database lookup independent of `$HOME`). It does NOT hide the real
227/// body from coverage: with the toggle off, `get_agents_dir_or_error(
228/// leviath_core::paths::agents_dir())` runs (and is measured) in every ordinary test, and
229/// only computes a `PathBuf` (no filesystem writes). The `None` arm of
230/// `get_agents_dir_or_error` is covered directly by
231/// `get_agents_dir_or_error_none_returns_error`.
232fn get_agents_dir() -> anyhow::Result<PathBuf> {
233    #[cfg(test)]
234    if FORCE_AGENTS_DIR_ERROR.with(|f| f.get()) {
235        anyhow::bail!("Could not determine home directory");
236    }
237    get_agents_dir_or_error(leviath_core::paths::agents_dir())
238}
239
240#[cfg(test)]
241thread_local! {
242    /// Test-only toggle letting `execute_returns_err_when_agents_dir_unresolvable`
243    /// force `get_agents_dir`'s `Err` arm deterministically.
244    static FORCE_AGENTS_DIR_ERROR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::test_support::write_test_agent;
251
252    fn write_manifest(dir: &Path, name: &str) {
253        write_manifest_with_description(dir, name, "Test agent");
254    }
255
256    fn write_manifest_with_description(dir: &Path, name: &str, description: &str) {
257        let content = format!(
258            r#"[agent]
259name = "{}"
260version = "1.0.0"
261description = "{}"
262
263[stages.main]
264mode = "autonomous"
265model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
266description = "Main"
267max_iterations = 5
268
269[context.regions]
270system = {{ kind = "pinned", max_tokens = 1000 }}
271"#,
272            name, description
273        );
274        write_test_agent(dir, content);
275    }
276
277    #[test]
278    fn read_agent_info_valid_manifest() {
279        let dir = tempfile::tempdir().unwrap();
280        write_manifest(dir.path(), "my-agent");
281        let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
282        assert_eq!(info.name, "my-agent");
283        assert_eq!(info.version, "1.0.0");
284        assert_eq!(info.description, "Test agent");
285    }
286
287    #[test]
288    fn read_agent_info_missing_file_returns_none() {
289        let result = read_agent_info(Path::new("/nonexistent/agent.leviath"));
290        assert!(result.is_none());
291    }
292
293    #[test]
294    fn read_agent_info_invalid_toml_returns_none() {
295        let dir = tempfile::tempdir().unwrap();
296        fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
297        let result = read_agent_info(&dir.path().join("agent.leviath"));
298        assert!(result.is_none());
299    }
300
301    #[test]
302    fn scan_directory_nonexistent_returns_empty() {
303        let agents = scan_directory_for_agents(Path::new("/nonexistent/path"));
304        assert!(agents.is_empty());
305    }
306
307    #[test]
308    fn scan_directory_path_is_a_file_returns_empty() {
309        // `dir.exists()` is true for a plain file too, so this reaches
310        // `fs::read_dir(dir)` - which fails with "not a directory",
311        // exercising the `if let Ok(entries) = ...` construct's implicit
312        // (no-`else`) false arm that no other test hits.
313        let tmp = tempfile::tempdir().unwrap();
314        let file_path = tmp.path().join("not-a-directory.txt");
315        fs::write(&file_path, "hello").unwrap();
316        let agents = scan_directory_for_agents(&file_path);
317        assert!(agents.is_empty());
318    }
319
320    #[test]
321    fn scan_directory_direct_manifest_invalid_is_skipped() {
322        // The direct-manifest branch (as opposed to the subdirectory-scan
323        // branch, covered separately by `scan_directory_subdir_with_invalid_manifest`)
324        // has its own `if let Some(info) = read_agent_info(...)` - this
325        // exercises that branch's `None` arm when the manifest at the
326        // directory's own root is present but unparseable.
327        let dir = tempfile::tempdir().unwrap();
328        fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
329        let agents = scan_directory_for_agents(dir.path());
330        assert!(agents.is_empty());
331    }
332
333    #[test]
334    fn scan_directory_with_direct_manifest() {
335        let dir = tempfile::tempdir().unwrap();
336        write_manifest(dir.path(), "direct-agent");
337        let agents = scan_directory_for_agents(dir.path());
338        assert_eq!(agents.len(), 1);
339        assert_eq!(agents[0].1.name, "direct-agent");
340    }
341
342    #[test]
343    fn scan_directory_with_subdirectories() {
344        let dir = tempfile::tempdir().unwrap();
345        let sub1 = dir.path().join("agent-a");
346        let sub2 = dir.path().join("agent-b");
347        fs::create_dir_all(&sub1).unwrap();
348        fs::create_dir_all(&sub2).unwrap();
349        write_manifest(&sub1, "agent-a");
350        write_manifest(&sub2, "agent-b");
351
352        let agents = scan_directory_for_agents(dir.path());
353        assert_eq!(agents.len(), 2);
354        let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
355        assert!(names.contains(&"agent-a"));
356        assert!(names.contains(&"agent-b"));
357    }
358
359    #[test]
360    fn scan_directory_ignores_subdirs_without_manifest() {
361        let dir = tempfile::tempdir().unwrap();
362        let sub = dir.path().join("no-manifest");
363        fs::create_dir_all(&sub).unwrap();
364        fs::write(sub.join("readme.txt"), "not a manifest").unwrap();
365
366        let agents = scan_directory_for_agents(dir.path());
367        assert!(agents.is_empty());
368    }
369
370    #[test]
371    fn list_args_default_filter() {
372        let args = ListArgs {
373            filter: "all".to_string(),
374        };
375        assert_eq!(args.filter, "all");
376    }
377
378    // ─── read_agent_info: description and version ───────────────────────
379
380    #[test]
381    fn read_agent_info_extracts_description() {
382        let dir = tempfile::tempdir().unwrap();
383        write_manifest(dir.path(), "my-agent");
384        let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
385        assert_eq!(info.description, "Test agent");
386        assert_eq!(info.version, "1.0.0");
387    }
388
389    // ─── scan_directory: nested but not deep ────────────────────────────
390
391    #[test]
392    fn scan_directory_with_both_direct_and_subdirs() {
393        let dir = tempfile::tempdir().unwrap();
394        // Direct manifest
395        write_manifest(dir.path(), "root-agent");
396        // Subdirectory with manifest
397        let sub = dir.path().join("child");
398        fs::create_dir_all(&sub).unwrap();
399        write_manifest(&sub, "child-agent");
400
401        let agents = scan_directory_for_agents(dir.path());
402        assert_eq!(agents.len(), 2);
403        let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
404        assert!(names.contains(&"root-agent"));
405        assert!(names.contains(&"child-agent"));
406    }
407
408    // ─── scan_directory: empty directory ────────────────────────────────
409
410    #[test]
411    fn scan_directory_empty_dir() {
412        let dir = tempfile::tempdir().unwrap();
413        let agents = scan_directory_for_agents(dir.path());
414        assert!(agents.is_empty());
415    }
416
417    // ─── scan_directory: subdirectory with invalid manifest ─────────────
418
419    #[test]
420    fn scan_directory_subdir_with_invalid_manifest() {
421        let dir = tempfile::tempdir().unwrap();
422        let sub = dir.path().join("bad-agent");
423        fs::create_dir_all(&sub).unwrap();
424        fs::write(sub.join("agent.leviath"), "invalid toml {{{{").unwrap();
425
426        let agents = scan_directory_for_agents(dir.path());
427        assert!(agents.is_empty());
428    }
429
430    // ─── get_agents_dir ────────────────────────────────────────────────
431
432    #[test]
433    fn get_agents_dir_returns_path_with_agents() {
434        let dir = get_agents_dir().unwrap();
435        assert!(dir.to_str().unwrap().contains(".leviath"));
436        assert!(dir.to_str().unwrap().ends_with("agents"));
437    }
438
439    #[test]
440    fn get_agents_dir_or_error_some_returns_path() {
441        let dir = PathBuf::from("/home/testuser/.leviath/agents");
442        assert_eq!(get_agents_dir_or_error(Some(dir.clone())).unwrap(), dir);
443    }
444
445    #[test]
446    fn get_agents_dir_or_error_none_returns_error() {
447        let err = get_agents_dir_or_error(None).unwrap_err();
448        assert!(
449            err.to_string()
450                .contains("Could not determine home directory")
451        );
452    }
453
454    // ─── read_agent_info: minimal manifest ──────────────────────────────
455
456    #[test]
457    fn read_agent_info_minimal_manifest() {
458        let dir = tempfile::tempdir().unwrap();
459        let content = r#"[agent]
460name = "minimal"
461version = "0.0.1"
462description = ""
463
464[stages.main]
465mode = "autonomous"
466model = { provider = "anthropic", model = "claude-sonnet-4-6" }
467description = "Main"
468max_iterations = 5
469
470[context.regions]
471system = { kind = "pinned", max_tokens = 1000 }
472"#;
473        write_test_agent(dir.path(), content);
474        let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
475        assert_eq!(info.name, "minimal");
476        assert_eq!(info.description, "");
477    }
478
479    // ─── execute() smoke test (real environment) ────────────────────────
480
481    #[tokio::test]
482    async fn execute_runs_without_error() {
483        // Isolated: this reaches `Config::load()`, which reads process-wide
484        // environment. Unisolated it races every `temp_env` test in the binary.
485        crate::config::with_isolated_config_path_async("list-runs-ok", |_fake_dir| async move {
486            // Touches the real environment (home dir / CWD / exe location /
487            // config) but must always succeed regardless of what it finds.
488            let args = ListArgs {
489                filter: "all".to_string(),
490            };
491            let result = execute(args).await;
492            assert!(result.is_ok());
493        })
494        .await;
495    }
496
497    #[tokio::test]
498    async fn execute_returns_err_when_agents_dir_unresolvable() {
499        // Isolated: this reaches `Config::load()`, which reads process-wide
500        // environment. Unisolated it races every `temp_env` test in the binary.
501        crate::config::with_isolated_config_path_async("list-dir-err", |_fake_dir| async move {
502            // Drives `execute`'s `get_agents_dir()?` error-propagation branch
503            // for real via the test-only `FORCE_AGENTS_DIR_ERROR` toggle on
504            // `get_agents_dir`'s twin (see its doc comment for why the real
505            // implementation's failure can't be forced directly).
506            FORCE_AGENTS_DIR_ERROR.with(|f| f.set(true));
507            let args = ListArgs {
508                filter: "all".to_string(),
509            };
510            let result = execute(args).await;
511            FORCE_AGENTS_DIR_ERROR.with(|f| f.set(false));
512
513            let err = result.unwrap_err();
514            assert!(
515                err.to_string()
516                    .contains("Could not determine home directory")
517            );
518        })
519        .await;
520    }
521
522    // `execute`'s `std::env::current_dir().unwrap_or_default()` can only take
523    // its `Err` arm in a real (if rare) TOCTOU scenario: the process's CWD is
524    // removed out from under it. That's genuinely reproducible on Unix (not a
525    // fake): create a directory, `chdir` into it, then delete it --
526    // `current_dir()` then reliably returns an error. On Windows this same
527    // sequence isn't reproducible: NTFS/Win32 refuse to remove a directory
528    // that's a live process's current working directory (a sharing
529    // violation), so `remove_dir_all` itself fails there instead of
530    // succeeding - confirmed via real Windows CI. Unix-only.
531    #[cfg(unix)]
532    #[tokio::test]
533    async fn execute_falls_back_to_default_cwd_when_current_dir_is_gone() {
534        // Isolated: this reaches `Config::load()`, which reads process-wide
535        // environment. Unisolated it races every `temp_env` test in the binary.
536        crate::config::with_isolated_config_path_async("list-cwd-gone", |_fake_dir| async move {
537            // `isolate_cwd_for_test` serializes against every other CWD-mutating
538            // test in the crate and restores CWD automatically on drop, so it's
539            // safe to hold across the `.await` below.
540            let _guard = crate::config::isolate_cwd_for_test();
541            let dir = std::env::temp_dir().join("lev-test-list-cwd-gone");
542            let _ = std::fs::remove_dir_all(&dir);
543            std::fs::create_dir_all(&dir).unwrap();
544            std::env::set_current_dir(&dir).unwrap();
545            std::fs::remove_dir_all(&dir).unwrap();
546
547            let args = ListArgs {
548                filter: "all".to_string(),
549            };
550            let result = execute(args).await;
551
552            assert!(result.is_ok());
553        })
554        .await;
555    }
556
557    /// Cross-platform companion to the Unix-only real-filesystem test above:
558    /// forces [`resolve_cwd`]'s `Err` arm deterministically via
559    /// [`FORCE_CWD_ERROR`] so `execute`'s `unwrap_or_default()` fallback is
560    /// also exercised on Windows, where the real filesystem race isn't
561    /// reproducible.
562    #[tokio::test]
563    async fn execute_falls_back_to_default_cwd_via_forced_error() {
564        // Isolated: this reaches `Config::load()`, which reads process-wide
565        // environment. Unisolated it races every `temp_env` test in the binary.
566        crate::config::with_isolated_config_path_async("list-cwd-forced", |_fake_dir| async move {
567            FORCE_CWD_ERROR.with(|f| f.set(true));
568            let args = ListArgs {
569                filter: "all".to_string(),
570            };
571            let result = execute(args).await;
572            FORCE_CWD_ERROR.with(|f| f.set(false));
573
574            assert!(result.is_ok());
575        })
576        .await;
577    }
578
579    /// A config that exists but doesn't parse must fail the command, not
580    /// silently list from the default `agent_paths` (regression: this used to
581    /// be `unwrap_or_default()`, which hid the user's agent directories with
582    /// no hint why).
583    #[tokio::test]
584    async fn execute_fails_loudly_on_a_broken_config() {
585        crate::config::with_isolated_config_path_async(
586            "list-broken-config",
587            |fake_dir| async move {
588                std::fs::write(fake_dir.join("config.toml"), "not = valid = toml").unwrap();
589                let args = ListArgs {
590                    filter: "all".to_string(),
591                };
592                let err = execute(args).await.expect_err("broken config must error");
593                assert!(err.to_string().contains("parse"), "{err}");
594            },
595        )
596        .await;
597    }
598
599    // ─── print_agent_listing (fully injectable) ─────────────────────────
600
601    #[test]
602    fn print_agent_listing_nothing_installed() {
603        // The bundled catalog is always non-empty, so it must not count as
604        // "you have agents" - otherwise the get-started guidance would be
605        // suppressed for exactly the fresh install that needs it.
606        let agents_dir = tempfile::tempdir().unwrap();
607        let cwd = tempfile::tempdir().unwrap();
608        let config = Config::default();
609
610        let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
611        assert!(result.is_ok());
612    }
613
614    #[test]
615    fn print_agent_listing_finds_installed_agent() {
616        let agents_dir = tempfile::tempdir().unwrap();
617        let sub = agents_dir.path().join("installed-agent");
618        fs::create_dir_all(&sub).unwrap();
619        write_manifest(&sub, "installed-agent");
620
621        let cwd = tempfile::tempdir().unwrap();
622        let config = Config::default();
623
624        let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
625        assert!(result.is_ok());
626    }
627
628    #[test]
629    fn print_agent_listing_finds_local_manifest() {
630        let agents_dir = tempfile::tempdir().unwrap();
631        let cwd = tempfile::tempdir().unwrap();
632        write_manifest(cwd.path(), "local-agent");
633        let config = Config::default();
634
635        let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
636        assert!(result.is_ok());
637    }
638
639    #[test]
640    fn print_agent_listing_local_manifest_invalid_is_skipped() {
641        // The local-manifest section has its own `if let Some(info) = ...`
642        // construct with no `else`; this exercises its false arm (an
643        // existing but unparseable `agent.leviath` in the cwd), which
644        // `print_agent_listing_finds_local_manifest` (valid manifest) never
645        // reaches.
646        let agents_dir = tempfile::tempdir().unwrap();
647        let cwd = tempfile::tempdir().unwrap();
648        fs::write(cwd.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
649        let config = Config::default();
650
651        let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
652        assert!(result.is_ok());
653    }
654
655    #[test]
656    fn print_agent_listing_finds_configured_path_agent() {
657        let agents_dir = tempfile::tempdir().unwrap();
658        let cwd = tempfile::tempdir().unwrap();
659        let configured = tempfile::tempdir().unwrap();
660        let sub = configured.path().join("configured-agent");
661        fs::create_dir_all(&sub).unwrap();
662        write_manifest(&sub, "configured-agent");
663
664        let config = Config {
665            agent_paths: vec![configured.path().to_path_buf()],
666            ..Config::default()
667        };
668
669        let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
670        assert!(result.is_ok());
671    }
672
673    #[test]
674    fn print_agent_listing_finds_builtin_agents() {
675        // An `agents/` directory beside the executable contributes a blueprint
676        // the embedded catalog doesn't have, so it is appended to the list.
677        let agents_dir = tempfile::tempdir().unwrap();
678        let cwd = tempfile::tempdir().unwrap();
679        let exe_dir = tempfile::tempdir().unwrap();
680        let builtin_dir = exe_dir.path().join("agents");
681        let sub = builtin_dir.join("builtin-agent");
682        fs::create_dir_all(&sub).unwrap();
683        write_manifest(&sub, "builtin-agent");
684        let config = Config::default();
685
686        let result =
687            print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
688        assert!(result.is_ok());
689    }
690
691    #[test]
692    fn print_agent_listing_does_not_list_a_bundled_agent_twice() {
693        // Running from a git checkout puts the *same* blueprints both in the
694        // embedded catalog and in `<exe_dir>/agents`. Listing each one twice
695        // would be pure noise, so the on-disk scan only appends what the
696        // catalog doesn't already carry.
697        let bundled = &crate::bundled::BUNDLED_AGENTS[0];
698        let agents_dir = tempfile::tempdir().unwrap();
699        let cwd = tempfile::tempdir().unwrap();
700        let exe_dir = tempfile::tempdir().unwrap();
701        let sub = exe_dir.path().join("agents").join(bundled.name);
702        fs::create_dir_all(&sub).unwrap();
703        crate::bundled::install_bundled(bundled, &exe_dir.path().join("agents")).unwrap();
704        let config = Config::default();
705
706        let result =
707            print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
708
709        assert!(result.is_ok());
710        // The same name+version pair the catalog already holds resolves to one
711        // entry, not two.
712        let entry = format!("{} (v{})", bundled.name, bundled.version);
713        let names: Vec<String> = crate::bundled::BUNDLED_AGENTS
714            .iter()
715            .map(|a| format!("{} (v{})", a.name, a.version))
716            .collect();
717        assert_eq!(names.iter().filter(|n| **n == entry).count(), 1);
718    }
719
720    #[test]
721    fn print_agent_listing_all_sources_populated() {
722        let agents_dir = tempfile::tempdir().unwrap();
723        fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
724        write_manifest(&agents_dir.path().join("installed"), "installed");
725
726        let cwd = tempfile::tempdir().unwrap();
727        write_manifest(cwd.path(), "local");
728
729        let configured = tempfile::tempdir().unwrap();
730        fs::create_dir_all(configured.path().join("configured")).unwrap();
731        write_manifest(&configured.path().join("configured"), "configured");
732
733        let exe_dir = tempfile::tempdir().unwrap();
734        let builtin_sub = exe_dir.path().join("agents").join("builtin");
735        fs::create_dir_all(&builtin_sub).unwrap();
736        write_manifest(&builtin_sub, "builtin");
737
738        let config = Config {
739            agent_paths: vec![configured.path().to_path_buf()],
740            ..Config::default()
741        };
742
743        let result =
744            print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
745        assert!(result.is_ok());
746    }
747
748    #[test]
749    fn print_agent_listing_empty_descriptions_across_all_sources() {
750        // Every section (installed / local / configured-path) has its own
751        // "empty description -> no dash suffix" branch; the tests above only
752        // ever exercise the non-empty path for all three, since
753        // `write_manifest` hardcodes a non-empty description.
754        let agents_dir = tempfile::tempdir().unwrap();
755        fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
756        write_manifest_with_description(&agents_dir.path().join("installed"), "installed", "");
757
758        let cwd = tempfile::tempdir().unwrap();
759        write_manifest_with_description(cwd.path(), "local", "");
760
761        let configured = tempfile::tempdir().unwrap();
762        fs::create_dir_all(configured.path().join("configured")).unwrap();
763        write_manifest_with_description(&configured.path().join("configured"), "configured", "");
764
765        let config = Config {
766            agent_paths: vec![configured.path().to_path_buf()],
767            ..Config::default()
768        };
769
770        let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
771        assert!(result.is_ok());
772    }
773
774    // ─── scan_directory: agent with empty description ────────────────────
775
776    #[test]
777    fn scan_directory_agent_with_empty_description() {
778        let dir = tempfile::tempdir().unwrap();
779        let sub = dir.path().join("my-agent");
780        fs::create_dir_all(&sub).unwrap();
781        let content = r#"[agent]
782name = "my-agent"
783version = "2.0.0"
784description = ""
785
786[stages.main]
787mode = "autonomous"
788model = { provider = "anthropic", model = "claude-sonnet-4-6" }
789description = "Main"
790max_iterations = 5
791
792[context.regions]
793system = { kind = "pinned", max_tokens = 1000 }
794"#;
795        write_test_agent(sub, content);
796
797        let agents = scan_directory_for_agents(dir.path());
798        assert_eq!(agents.len(), 1);
799        assert_eq!(agents[0].1.description, "");
800    }
801
802    // ─── scan_directory: multiple subdirs with mixed manifests ──────────
803
804    #[test]
805    fn scan_directory_mixed_valid_and_invalid() {
806        let dir = tempfile::tempdir().unwrap();
807        let good = dir.path().join("good");
808        let bad = dir.path().join("bad");
809        let empty = dir.path().join("empty");
810        fs::create_dir_all(&good).unwrap();
811        fs::create_dir_all(&bad).unwrap();
812        fs::create_dir_all(&empty).unwrap();
813
814        write_manifest(&good, "good-agent");
815        fs::write(bad.join("agent.leviath"), "bad {{ toml").unwrap();
816
817        let agents = scan_directory_for_agents(dir.path());
818        assert_eq!(agents.len(), 1);
819        assert_eq!(agents[0].1.name, "good-agent");
820    }
821}