//! Tests for job files and the run ledger. Do not edit. use bxcore::{SessionId, Timestamp}; use loopd::jobs::{self, JobsError, RunStatus}; fn ts() -> Timestamp { Timestamp::parse("2026-10-03T07:00:00.000Z").unwrap() } struct Scratch { dir: std::path::PathBuf, } impl Scratch { fn new() -> Scratch { let n = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); let dir = std::env::temp_dir().join(format!("loopd-jobs-{}-{n}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); Scratch { dir } } } impl Drop for Scratch { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.dir); } } #[test] fn a_valid_job_file_loads() { let home = Scratch::new(); let jobs = home.dir.join("jobs"); std::fs::create_dir(&jobs).unwrap(); std::fs::write( jobs.join("mail.toml"), "schedule = \"0 7 * * *\"\nprompt = \"Check mail.\"\n", ) .unwrap(); let loaded = jobs::load(&jobs).unwrap(); assert_eq!(loaded.len(), 1); assert_eq!(loaded[0].name, "mail"); assert_eq!(loaded[0].schedule, "0 7 * * *"); assert_eq!(loaded[0].prompt, "Check mail."); } #[test] fn unknown_keys_are_errors_with_the_runbook() { let home = Scratch::new(); let jobs = home.dir.join("jobs"); std::fs::create_dir(&jobs).unwrap(); std::fs::write( jobs.join("mail.toml"), "schedule = \"* * * * *\"\nprompt = \"x\"\nwhen = \"now\"\n", ) .unwrap(); let err = jobs::load(&jobs).unwrap_err(); let s = err.to_string(); assert!(s.contains("mail.toml"), "{s}"); assert!(s.ends_with("see docs/runbook.md#jobs-invalid"), "{s}"); } #[test] fn a_bad_name_is_an_error() { let home = Scratch::new(); let jobs = home.dir.join("jobs"); std::fs::create_dir(&jobs).unwrap(); std::fs::write( jobs.join("Mail.toml"), "schedule = \"* * * * *\"\nprompt = \"x\"\n", ) .unwrap(); let err = jobs::load(&jobs).unwrap_err(); assert!(matches!(err, JobsError::Name(_)), "{err:?}"); } #[test] fn session_id_fits_and_is_lowercase() { let id = jobs::session_id("mail", ts()).unwrap(); assert_eq!(id.as_str(), "job-mail-20261003t0700"); SessionId::new(id.as_str()).unwrap(); } #[test] fn a_name_too_long_for_a_session_id_is_an_error() { let long = "n".repeat(50); let err = jobs::session_id(&long, ts()).unwrap_err(); assert!(matches!(err, JobsError::Name(_)), "{err:?}"); } #[test] fn a_missing_jobs_directory_is_empty() { let home = Scratch::new(); let loaded = jobs::load(&home.dir.join("jobs")).unwrap(); assert!(loaded.is_empty()); } #[test] fn leftover_started_is_closed_as_failed() { let home = Scratch::new(); let path = home.dir.join("runs.jsonl"); let session = jobs::session_id("mail", ts()).unwrap(); jobs::append( &path, &jobs::Run { time: ts(), name: "mail".to_string(), session: session.clone(), status: RunStatus::Started, }, ) .unwrap(); let n = jobs::close_leftovers(&path, ts()).unwrap(); assert_eq!(n, 1); assert!(jobs::live_session(&path, "mail").unwrap().is_none()); } #[test] fn a_started_run_without_a_close_is_live() { let home = Scratch::new(); let path = home.dir.join("runs.jsonl"); let session = jobs::session_id("mail", ts()).unwrap(); jobs::append( &path, &jobs::Run { time: ts(), name: "mail".to_string(), session: session.clone(), status: RunStatus::Started, }, ) .unwrap(); assert_eq!( jobs::live_session(&path, "mail").unwrap().as_ref(), Some(&session) ); jobs::append( &path, &jobs::Run { time: ts(), name: "mail".to_string(), session, status: RunStatus::Finished, }, ) .unwrap(); assert!(jobs::live_session(&path, "mail").unwrap().is_none()); }