KeiSeiKit-1.0/_primitives/_rust/kei-scheduler/tests/scheduler_integration.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

189 lines
6.6 KiB
Rust

//! Integration tests for kei-scheduler. Uses `Store::open_memory` so
//! each test owns a throwaway DB and a deterministic wall clock
//! (`now` passed explicitly where the API allows).
//!
//! `schedule()` + `cancel()` internally read `Utc::now()` once; that's
//! fine because we check relative ordering (`next_run_at` compared to
//! a post-call `Utc::now()` lower bound), not absolute values.
use chrono::Utc;
use kei_scheduler::{
cancel, compute_next, get_task, list_due, mark_run, open_memory, schedule,
task_status, ParseError, Store, AT, CRON, INTERVAL,
};
fn store() -> Store {
open_memory().expect("in-memory store opens clean")
}
#[test]
fn cron_schedule_sets_future_next_run() {
let s = store();
let before = Utc::now().timestamp();
let id = schedule(s.conn(), "cron1", CRON, "*/5 * * * *", "echo").unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
assert_eq!(t.name, "cron1");
assert_eq!(t.trigger_kind, "cron");
assert_eq!(t.status, task_status::PENDING);
let next = t.next_run_at.expect("cron trigger must produce a next_run_at");
assert!(next >= before, "next_run_at {next} must be >= launch time {before}");
}
#[test]
fn at_schedule_with_future_ts_matches_iso_parse() {
let s = store();
// 2030-01-01T00:00:00Z → unix 1893456000 (verified via
// chrono::DateTime::parse_from_rfc3339 at test time).
let id = schedule(s.conn(), "at1", AT, "2030-01-01T00:00:00Z", "echo").unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
assert_eq!(t.next_run_at, Some(1_893_456_000));
assert_eq!(t.trigger_kind, "at");
}
#[test]
fn interval_schedule_sets_now_plus_secs() {
let s = store();
let before = Utc::now().timestamp();
let id = schedule(s.conn(), "int1", INTERVAL, "3600", "echo").unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
let next = t.next_run_at.expect("interval trigger must set next_run_at");
let after = Utc::now().timestamp();
assert!(next >= before + 3600);
assert!(next <= after + 3600);
}
#[test]
fn cancel_sets_status_and_clears_next_run() {
let s = store();
let id = schedule(s.conn(), "tcan", INTERVAL, "60", "echo").unwrap();
cancel(s.conn(), id).unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
assert_eq!(t.status, task_status::CANCELLED);
assert_eq!(t.next_run_at, None);
}
#[test]
fn list_due_returns_eligible_pending_tasks() {
let s = store();
// An interval with spec=60 produces next_run ≈ now+60. Query with
// now+120 to make sure it's due.
schedule(s.conn(), "due1", INTERVAL, "60", "echo").unwrap();
let query_ts = Utc::now().timestamp() + 120;
let due = list_due(s.conn(), query_ts).unwrap();
assert_eq!(due.len(), 1);
assert_eq!(due[0].name, "due1");
assert_eq!(due[0].status, task_status::PENDING);
}
#[test]
fn list_due_excludes_cancelled_tasks() {
let s = store();
let id = schedule(s.conn(), "cx", INTERVAL, "60", "echo").unwrap();
cancel(s.conn(), id).unwrap();
let due = list_due(s.conn(), Utc::now().timestamp() + 100_000).unwrap();
assert!(due.is_empty(), "cancelled tasks must not appear in list_due");
}
#[test]
fn mark_run_on_interval_advances_next_run() {
let s = store();
let id = schedule(s.conn(), "mrint", INTERVAL, "3600", "echo").unwrap();
let now = 2_000_000_000;
mark_run(s.conn(), id, 0, now).unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
assert_eq!(t.last_run_at, Some(now));
assert_eq!(t.last_exit_code, Some(0));
assert_eq!(t.next_run_at, Some(now + 3600));
assert_eq!(t.status, task_status::SCHEDULED);
}
#[test]
fn mark_run_on_at_completes_task() {
let s = store();
let id = schedule(s.conn(), "mrat", AT, "2030-01-01T00:00:00Z", "echo").unwrap();
mark_run(s.conn(), id, 0, 1_893_456_005).unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
assert_eq!(t.status, task_status::DONE);
assert_eq!(t.next_run_at, None);
assert_eq!(t.last_exit_code, Some(0));
}
#[test]
fn mark_run_on_cron_recomputes_next() {
let s = store();
let id = schedule(s.conn(), "mrcron", CRON, "*/5 * * * *", "echo").unwrap();
let now: i64 = 2_000_000_000;
mark_run(s.conn(), id, 0, now).unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
let next = t.next_run_at.expect("cron mark_run must recompute next_run_at");
// `*/5 * * * *` = every 5 minutes at second-0; next must be within
// 300 seconds of `now` and strictly greater.
assert!(next > now);
assert!(next <= now + 300, "next {next} must be within 5m of now {now}");
assert_eq!(t.status, task_status::SCHEDULED);
}
#[test]
fn mark_run_with_nonzero_exit_at_sets_failed() {
let s = store();
let id = schedule(s.conn(), "failat", AT, "2030-06-15T12:00:00Z", "echo").unwrap();
mark_run(s.conn(), id, 17, 1_910_000_000).unwrap();
let t = get_task(s.conn(), id).unwrap().unwrap();
assert_eq!(t.status, task_status::FAILED);
assert_eq!(t.last_exit_code, Some(17));
assert_eq!(t.next_run_at, None);
}
#[test]
fn invalid_cron_returns_parse_error() {
let err = compute_next(CRON, "not a cron expression", 0).unwrap_err();
assert!(
matches!(err, ParseError::InvalidCron(_, _)),
"expected InvalidCron, got {err:?}"
);
}
#[test]
fn invalid_iso_datetime_returns_parse_error() {
let err = compute_next(AT, "not-a-date", 0).unwrap_err();
assert!(
matches!(err, ParseError::InvalidIsoDatetime(_)),
"expected InvalidIsoDatetime, got {err:?}"
);
}
#[test]
fn malformed_trigger_kind_is_rejected() {
let s = store();
let err = schedule(s.conn(), "bad", "weekly", "whatever", "echo")
.expect_err("unknown kind must fail");
assert!(
matches!(err, kei_scheduler::Error::Parse(ParseError::UnknownKind(_))),
"expected UnknownKind, got {err:?}"
);
}
#[test]
fn duplicate_name_is_rejected_typed() {
let s = store();
schedule(s.conn(), "dup", INTERVAL, "60", "echo").unwrap();
let err = schedule(s.conn(), "dup", INTERVAL, "120", "echo")
.expect_err("unique-name collision must fail");
assert!(
matches!(err, kei_scheduler::Error::NameExists(ref n) if n == "dup"),
"expected NameExists(dup), got {err:?}"
);
}
#[test]
fn zero_interval_is_rejected() {
let err = compute_next(INTERVAL, "0", 100).unwrap_err();
assert!(matches!(err, ParseError::InvalidInterval(_)));
}
#[test]
fn at_in_the_past_returns_none() {
// 2020-01-01 with `from = 2030-era` → no future fire.
let next = compute_next(AT, "2020-01-01T00:00:00Z", 1_893_456_000).unwrap();
assert_eq!(next, None);
}