use crate::scheduler::{
BoundedQueue, IndexJob, JobKind, JobState, QueueCapacity, QueueKind, QueueSet, ResourceMode,
Scheduler, SchedulerEvent, WorkPriority,
};
use orbok_core::SourceId;
fn src() -> SourceId {
SourceId::generate()
}
fn job(kind: JobKind) -> IndexJob {
IndexJob::new(src(), kind)
}
fn job_for(source_id: SourceId, kind: JobKind) -> IndexJob {
IndexJob::new(source_id, kind)
}
#[test]
fn queue_priority_ordering() {
let mut q = BoundedQueue::new(QueueKind::Extract, 100);
let low = job(JobKind::ExtractFile).with_priority(WorkPriority::LowBackground);
let high = job(JobKind::ExtractFile).with_priority(WorkPriority::UserBlocking);
let mid = job(JobKind::ExtractFile).with_priority(WorkPriority::NormalBackground);
q.push(low);
q.push(high);
q.push(mid);
assert_eq!(q.pop().unwrap().priority, WorkPriority::UserBlocking);
assert_eq!(q.pop().unwrap().priority, WorkPriority::NormalBackground);
assert_eq!(q.pop().unwrap().priority, WorkPriority::LowBackground);
}
#[test]
fn equal_priority_is_fifo() {
let mut q = BoundedQueue::new(QueueKind::Extract, 100);
let a = job(JobKind::ExtractFile).with_priority(WorkPriority::NormalBackground);
let b = job(JobKind::ExtractFile).with_priority(WorkPriority::NormalBackground);
let a_id = a.id.clone();
let b_id = b.id.clone();
q.push(a);
q.push(b);
assert_eq!(q.pop().unwrap().id, a_id, "first-in should be first-out");
assert_eq!(q.pop().unwrap().id, b_id);
}
#[test]
fn queue_capacity_enforced() {
let cap = 3;
let mut q = BoundedQueue::new(QueueKind::Extract, cap);
for _ in 0..cap {
assert!(!q.is_full());
q.push(job(JobKind::ExtractFile));
}
assert!(q.is_full());
assert_eq!(q.len(), cap);
}
#[test]
fn enqueue_full_queue_returns_backpressure_error() {
let mut q = BoundedQueue::new(QueueKind::Extract, 1);
q.push(job(JobKind::ExtractFile)); assert!(q.is_full());
assert!(q.is_full(), "caller must check is_full before pushing");
}
#[test]
fn embedding_skipped_when_user_active() {
let cap = QueueCapacity::default();
let mut qs = QueueSet::new(&cap);
qs.embedding.push(job(JobKind::GenerateEmbedding));
let got = qs.pop_next(ResourceMode::Normal);
assert!(got.is_some(), "Normal mode: embedding should run");
qs.embedding.push(job(JobKind::GenerateEmbedding));
let got_active = qs.pop_next(ResourceMode::UserActive);
assert!(
got_active.is_none(),
"UserActive mode: embedding must be skipped"
);
}
#[test]
fn extract_runs_in_user_active_mode() {
let cap = QueueCapacity::default();
let mut qs = QueueSet::new(&cap);
qs.extract.push(job(JobKind::ExtractFile));
let got = qs.pop_next(ResourceMode::UserActive);
assert!(got.is_some(), "extract must run even in UserActive mode");
assert_eq!(got.unwrap().kind, JobKind::ExtractFile);
}
#[test]
fn tick_returns_none_when_paused() {
let mut sched = Scheduler::with_defaults();
let _source = src();
sched.notify_user_idle();
assert!(sched.tick().is_none(), "no jobs → None");
}
#[test]
fn resource_mode_transitions() {
let mut sched = Scheduler::with_defaults();
assert_eq!(sched.resource_mode(), ResourceMode::Normal);
sched.notify_user_active();
assert_eq!(sched.resource_mode(), ResourceMode::UserActive);
sched.notify_user_idle();
assert_eq!(sched.resource_mode(), ResourceMode::Normal);
}
#[test]
fn user_activity_event_emitted() {
let mut sched = Scheduler::with_defaults();
sched.notify_user_active();
let events = sched.drain_events();
assert!(
events
.iter()
.any(|e| matches!(e, SchedulerEvent::UserActivityDetected)),
"UserActivityDetected must be emitted"
);
assert!(
events.iter().any(|e| matches!(
e,
SchedulerEvent::ResourceModeChanged(ResourceMode::UserActive)
)),
"ResourceModeChanged(UserActive) must be emitted"
);
}
#[test]
fn idle_event_emitted() {
let mut sched = Scheduler::with_defaults();
sched.notify_user_active();
sched.drain_events(); sched.notify_user_idle();
let events = sched.drain_events();
assert!(
events
.iter()
.any(|e| matches!(e, SchedulerEvent::ResourceModeChanged(ResourceMode::Normal))),
"ResourceModeChanged(Normal) must be emitted on idle"
);
}
#[test]
fn repeated_user_active_does_not_spam_events() {
let mut sched = Scheduler::with_defaults();
sched.notify_user_active();
sched.notify_user_active(); sched.notify_user_active();
let events = sched.drain_events();
let activity_count = events
.iter()
.filter(|e| matches!(e, SchedulerEvent::UserActivityDetected))
.count();
assert_eq!(
activity_count, 1,
"only one UserActivityDetected per transition"
);
}
#[test]
fn cancel_source_removes_jobs_from_queue() {
let mut q = BoundedQueue::new(QueueKind::Extract, 100);
let target = src();
let other = src();
q.push(job_for(target.clone(), JobKind::ExtractFile));
q.push(job_for(target.clone(), JobKind::ExtractFile));
q.push(job_for(other.clone(), JobKind::ExtractFile));
let removed = q.cancel_for_source(&target);
assert_eq!(removed, 2, "two target jobs should be removed");
assert_eq!(q.len(), 1, "one unrelated job must remain");
assert_eq!(q.peek().unwrap().source_id, other);
}
#[test]
fn queue_set_cancel_source_removes_across_queues() {
let cap = QueueCapacity::default();
let mut qs = QueueSet::new(&cap);
let target = src();
qs.scan.push(job_for(target.clone(), JobKind::ScanSource));
qs.extract
.push(job_for(target.clone(), JobKind::ExtractFile));
qs.embedding
.push(job_for(target.clone(), JobKind::GenerateEmbedding));
qs.extract.push(job_for(src(), JobKind::ExtractFile));
let removed = qs.cancel_source(&target);
assert_eq!(removed, 3, "all three target jobs should be cancelled");
assert_eq!(qs.total_pending(), 1, "one unrelated job must remain");
}
#[test]
fn work_priority_ord_is_correct() {
assert!(WorkPriority::UserBlocking > WorkPriority::UserVisible);
assert!(WorkPriority::UserVisible > WorkPriority::NormalBackground);
assert!(WorkPriority::NormalBackground > WorkPriority::LowBackground);
assert!(WorkPriority::LowBackground > WorkPriority::Maintenance);
}
#[test]
fn embedding_default_priority_is_low() {
assert_eq!(
JobKind::GenerateEmbedding.default_priority(),
WorkPriority::LowBackground
);
}
#[test]
fn cleanup_default_priority_is_maintenance() {
assert_eq!(
JobKind::Cleanup.default_priority(),
WorkPriority::Maintenance
);
}
#[test]
fn new_job_is_pending() {
let j = IndexJob::new(src(), JobKind::ExtractFile);
assert_eq!(j.state, JobState::Pending);
assert_eq!(j.attempt_count, 0);
assert!(j.last_error_kind.is_none());
}
#[test]
fn queue_clear_removes_all() {
let mut q = BoundedQueue::new(QueueKind::Extract, 100);
q.push(job(JobKind::ExtractFile));
q.push(job(JobKind::ExtractFile));
let removed = q.clear();
assert_eq!(removed, 2);
assert!(q.is_empty());
}
#[test]
fn queue_set_total_pending() {
let cap = QueueCapacity::default();
let mut qs = QueueSet::new(&cap);
qs.scan.push(job(JobKind::ScanSource));
qs.extract.push(job(JobKind::ExtractFile));
qs.keyword.push(job(JobKind::UpdateKeywordIndex));
assert_eq!(qs.total_pending(), 3);
}