use std::sync::Arc;
use salvor_core::{Event, EventEnvelope, RunId, SequenceNumber};
use salvor_runtime::due_runs;
use salvor_store::{EventStore, SqliteStore};
use serde_json::json;
use time::macros::datetime;
use time::{Duration, OffsetDateTime};
const NOW: OffsetDateTime = datetime!(2026-08-07 12:00:00 UTC);
async fn seed(store: &dyn EventStore, events: Vec<Event>) -> RunId {
let run_id = RunId::new();
for (index, event) in events.into_iter().enumerate() {
let envelope = EventEnvelope::new(run_id, SequenceNumber::new(index as u64), NOW, event);
store.append(&envelope).await.expect("append");
}
run_id
}
fn started() -> Event {
Event::RunStarted {
agent_def_hash: "sha256:wake-selection".to_owned(),
input: json!("go"),
labels: None,
driven_by: None,
}
}
fn store() -> Arc<dyn EventStore> {
Arc::new(SqliteStore::in_memory().expect("store opens"))
}
#[tokio::test]
async fn only_sleeping_runs_past_their_deadline_are_due() {
let store = store();
let overdue = seed(
store.as_ref(),
vec![
started(),
Event::SleepStarted {
wake_at: NOW - Duration::hours(1),
},
],
)
.await;
let exactly_now = seed(
store.as_ref(),
vec![started(), Event::SleepStarted { wake_at: NOW }],
)
.await;
seed(
store.as_ref(),
vec![
started(),
Event::SleepStarted {
wake_at: NOW + Duration::hours(1),
},
],
)
.await;
seed(
store.as_ref(),
vec![
started(),
Event::SleepStarted {
wake_at: NOW - Duration::hours(2),
},
Event::SleepCompleted {},
],
)
.await;
seed(
store.as_ref(),
vec![
started(),
Event::SleepStarted {
wake_at: NOW - Duration::days(9),
},
Event::SleepCompleted {},
Event::RunCompleted {
output: json!("done"),
},
],
)
.await;
seed(store.as_ref(), vec![started()]).await;
let due = due_runs(store.as_ref(), NOW)
.await
.expect("selection reads");
let ids: Vec<RunId> = due.iter().map(|run| run.run_id).collect();
assert_eq!(
ids,
vec![overdue, exactly_now],
"exactly the two sleeping runs at or past their deadline, oldest first"
);
assert_eq!(
due[0].wake_at,
NOW - Duration::hours(1),
"the recorded deadline is carried, so a report need not fold the log again"
);
assert_eq!(due[1].wake_at, NOW);
}
#[tokio::test]
async fn a_deadline_that_has_not_arrived_selects_nothing_until_it_does() {
let store = store();
let wake_at = NOW + Duration::days(7);
let run_id = seed(
store.as_ref(),
vec![started(), Event::SleepStarted { wake_at }],
)
.await;
assert!(
due_runs(store.as_ref(), NOW)
.await
.expect("reads")
.is_empty(),
"a week out is not due now"
);
assert!(
due_runs(store.as_ref(), wake_at - Duration::seconds(1))
.await
.expect("reads")
.is_empty(),
"one second short is still not due; the comparison has no slack in it"
);
let due = due_runs(store.as_ref(), wake_at).await.expect("reads");
assert_eq!(due.len(), 1);
assert_eq!(due[0].run_id, run_id);
assert_eq!(due[0].wake_at, wake_at);
}
#[tokio::test]
async fn an_empty_store_has_nothing_due() {
let store = store();
assert!(
due_runs(store.as_ref(), NOW)
.await
.expect("reads")
.is_empty()
);
}
#[tokio::test]
async fn the_listing_is_ordered_by_deadline() {
let store = store();
let mut seeded = Vec::new();
for hours in [1_i64, 9, 3] {
let run_id = seed(
store.as_ref(),
vec![
started(),
Event::SleepStarted {
wake_at: NOW - Duration::hours(hours),
},
],
)
.await;
seeded.push((hours, run_id));
}
let due = due_runs(store.as_ref(), NOW).await.expect("reads");
let hours: Vec<i64> = due
.iter()
.map(|run| (NOW - run.wake_at).whole_hours())
.collect();
assert_eq!(hours, vec![9, 3, 1], "oldest deadline first");
assert_eq!(due.len(), seeded.len());
}