//! Tests for batch_get_last_run_status (#1469 N+1 fix). //! //! Verifies: //! 1. Empty input returns empty map //! 2. Returns the most recent run status per routine //! 3. Routines with no runs are omitted from result //! 4. Multiple routines with different statuses are correctly returned #[cfg(feature = "libsql")] mod tests { use std::sync::Arc; use chrono::{Duration, Utc}; use uuid::Uuid; use ironclaw::agent::routine::{ Routine, RoutineAction, RoutineGuardrails, RoutineRun, RunStatus, Trigger, }; use ironclaw::db::Database; async fn create_test_db() -> (Arc, tempfile::TempDir) { use ironclaw::db::libsql::LibSqlBackend; let temp_dir = tempfile::tempdir().expect("tempdir"); let db_path = temp_dir.path().join("test.db"); let backend = LibSqlBackend::new_local(&db_path) .await .expect("LibSqlBackend"); backend.run_migrations().await.expect("migrations"); let db: Arc = Arc::new(backend); (db, temp_dir) } fn make_routine(id: Uuid) -> Routine { Routine { id, name: format!("test-routine-{}", id), description: "Test routine".to_string(), user_id: "default".to_string(), enabled: true, trigger: Trigger::Manual, action: RoutineAction::FullJob { title: "Test job".to_string(), description: "Test description".to_string(), max_iterations: 5, }, guardrails: RoutineGuardrails { cooldown: std::time::Duration::from_secs(0), max_concurrent: 1, dedup_window: None, }, notify: Default::default(), last_run_at: None, next_fire_at: None, run_count: 0, consecutive_failures: 0, state: serde_json::json!({}), created_at: Utc::now(), updated_at: Utc::now(), } } fn make_run( routine_id: Uuid, status: RunStatus, started_at: chrono::DateTime, ) -> RoutineRun { RoutineRun { id: Uuid::new_v4(), routine_id, trigger_type: "manual".to_string(), trigger_detail: None, started_at, completed_at: if status == RunStatus::Running { None } else { Some(Utc::now()) }, status, result_summary: None, tokens_used: None, job_id: None, created_at: Utc::now(), } } #[tokio::test] async fn test_batch_get_last_run_status_empty_input() { let (db, _tmp) = create_test_db().await; let result = db .batch_get_last_run_status(&[]) .await .expect("batch query"); assert!(result.is_empty()); } #[tokio::test] async fn test_batch_get_last_run_status_returns_latest() { let (db, _tmp) = create_test_db().await; let routine_id = Uuid::new_v4(); db.create_routine(&make_routine(routine_id)) .await .expect("create routine"); // Create an older run with Ok status let older_run = make_run(routine_id, RunStatus::Ok, Utc::now() - Duration::hours(2)); db.create_routine_run(&older_run) .await .expect("create older run"); db.complete_routine_run(older_run.id, RunStatus::Ok, None, None) .await .expect("complete older run"); // Create a newer run with Attention status let newer_run = make_run( routine_id, RunStatus::Attention, Utc::now() - Duration::hours(1), ); db.create_routine_run(&newer_run) .await .expect("create newer run"); db.complete_routine_run(newer_run.id, RunStatus::Attention, None, None) .await .expect("complete newer run"); let result = db .batch_get_last_run_status(&[routine_id]) .await .expect("batch query"); assert_eq!(result.get(&routine_id), Some(&RunStatus::Attention)); } #[tokio::test] async fn test_batch_get_last_run_status_omits_routines_without_runs() { let (db, _tmp) = create_test_db().await; let with_runs = Uuid::new_v4(); let without_runs = Uuid::new_v4(); db.create_routine(&make_routine(with_runs)) .await .expect("create routine"); db.create_routine(&make_routine(without_runs)) .await .expect("create routine"); let run = make_run(with_runs, RunStatus::Ok, Utc::now()); db.create_routine_run(&run).await.expect("create run"); db.complete_routine_run(run.id, RunStatus::Ok, None, None) .await .expect("complete run"); let result = db .batch_get_last_run_status(&[with_runs, without_runs]) .await .expect("batch query"); assert_eq!(result.get(&with_runs), Some(&RunStatus::Ok)); assert_eq!(result.get(&without_runs), None); } #[tokio::test] async fn test_batch_get_last_run_status_multiple_routines() { let (db, _tmp) = create_test_db().await; let r1 = Uuid::new_v4(); let r2 = Uuid::new_v4(); db.create_routine(&make_routine(r1)) .await .expect("create r1"); db.create_routine(&make_routine(r2)) .await .expect("create r2"); let run1 = make_run(r1, RunStatus::Running, Utc::now()); db.create_routine_run(&run1).await.expect("create run1"); let run2 = make_run(r2, RunStatus::Failed, Utc::now()); db.create_routine_run(&run2).await.expect("create run2"); db.complete_routine_run(run2.id, RunStatus::Failed, None, None) .await .expect("complete run2"); let result = db .batch_get_last_run_status(&[r1, r2]) .await .expect("batch query"); assert_eq!(result.get(&r1), Some(&RunStatus::Running)); assert_eq!(result.get(&r2), Some(&RunStatus::Failed)); } }