diff --git a/src/main.rs b/src/main.rs index 2ff45bb..d5ea04f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -259,12 +259,14 @@ async fn run_job_command(config_path: &str, command: JobCommand) -> Result<()> { } JobCommand::List => { let catalog = jobs::Catalog::load(&cfg)?; + let mut rows = Vec::new(); for job in catalog.jobs.values() { - println!("{}\tvalid\t{}", job.name, job.backend.as_str()); + rows.push((job.name.as_str(), "valid", job.backend.as_str())); } - for error in catalog.errors { - println!("{}\tinvalid\t{}", error.name, error.message); + for error in &catalog.errors { + rows.push((error.name.as_str(), "invalid", error.message.as_str())); } + print_job_list(&rows); Ok(()) } JobCommand::Show(name) => { @@ -338,6 +340,49 @@ async fn run_job_command(config_path: &str, command: JobCommand) -> Result<()> { } } +fn print_job_list(rows: &[(&str, &str, &str)]) { + let rows = rows + .iter() + .map(|(name, status, detail)| { + ( + escape_table_cell(name), + escape_table_cell(status), + escape_table_cell(detail), + ) + }) + .collect::>(); + let name_width = rows + .iter() + .map(|(name, _, _)| name.len()) + .max() + .unwrap_or(0) + .max("NAME".len()); + let status_width = rows + .iter() + .map(|(_, status, _)| status.len()) + .max() + .unwrap_or(0) + .max("STATUS".len()); + + println!( + "{: String { + value.chars().flat_map(|c| c.escape_default()).collect() +} + fn report_invalid_jobs(cfg: &config::Config) -> Result<()> { let catalog = jobs::Catalog::load(cfg)?; for error in catalog.errors { diff --git a/tests/manual_job_crash.rs b/tests/manual_job_crash.rs index b280ff1..04233df 100644 --- a/tests/manual_job_crash.rs +++ b/tests/manual_job_crash.rs @@ -41,7 +41,9 @@ printf '%s\n' '{"type":"thread.started","thread_id":"cli-thread"}' let list = run_cli(binary, &config, &["job", "list"]); assert!(list.status.success()); - assert!(stdout(&list).contains("crash-test\tvalid\tcodex")); + let list_output = stdout(&list); + assert!(list_output.contains("NAME STATUS BACKEND / ERROR")); + assert!(list_output.contains("crash-test valid codex")); let show = run_cli(binary, &config, &["job", "show", "crash-test"]); assert!(show.status.success()); @@ -78,6 +80,29 @@ printf '%s\n' '{"type":"thread.started","thread_id":"cli-thread"}' assert!(stdout(&invalid).contains("INVALID\tinvalid")); assert!(String::from_utf8_lossy(&invalid.stderr).contains("1 invalid job(s)")); + let list_with_invalid = run_cli(binary, &config, &["job", "list"]); + assert!(list_with_invalid.status.success()); + assert!(stdout(&list_with_invalid) + .lines() + .any(|line| line.ends_with("invalid job must start with a +++ frontmatter delimiter"))); + + std::fs::write(jobs.join("bad\nname.md"), "not a runbook").unwrap(); + let list_with_malformed_name = run_cli(binary, &config, &["job", "list"]); + assert!(list_with_malformed_name.status.success()); + let malformed_output = stdout(&list_with_malformed_name); + assert_eq!(malformed_output.lines().count(), 5); + assert!(malformed_output.contains(r"bad\nname.md invalid ")); + + std::fs::remove_file(jobs.join("crash-test.md")).unwrap(); + std::fs::remove_file(jobs.join("invalid.md")).unwrap(); + std::fs::remove_file(jobs.join("bad\nname.md")).unwrap(); + let empty_list = run_cli(binary, &config, &["job", "list"]); + assert!(empty_list.status.success()); + assert_eq!( + stdout(&empty_list), + "NAME STATUS BACKEND / ERROR\n---- ------ ---------------\n" + ); + let _ = std::fs::remove_dir_all(root); }