diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 1352d6735..1726cee58 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -184,17 +184,34 @@ impl CardQueues { } } - /// Return the current due counts. If there are no due cards, the learning - /// cutoff is updated to the current time first, and any newly-due learning - /// cards are added to the counts. + /// Return the current due counts. If there are no due cards or if learning + /// cards have become due since the last cutoff update, the learning cutoff + /// is updated to the current time first, and any newly-due learning cards + /// are added to the counts. pub(crate) fn counts(&mut self) -> Counts { - if self.counts.all_zero() { + if self.counts.all_zero() || self.has_newly_due_learning_cards() { // we discard the returned undo information in this case self.update_learning_cutoff_and_count(); } self.counts } + /// Check if any learning cards have become due since the last cutoff + /// update. + fn has_newly_due_learning_cards(&self) -> bool { + let current_cutoff = self.current_learning_cutoff; + let now = TimestampSecs::now(); + + if now <= current_cutoff { + return false; + } + + let new_ahead_cutoff = now.adding_secs(self.learn_ahead_secs); + self.intraday_learning + .iter() + .any(|e| e.due > current_cutoff && e.due <= new_ahead_cutoff) + } + fn is_stale(&self, current_day: u32) -> bool { self.current_day != current_day } diff --git a/rslib/src/scheduler/queue/undo.rs b/rslib/src/scheduler/queue/undo.rs index 02046eebc..986d126d8 100644 --- a/rslib/src/scheduler/queue/undo.rs +++ b/rslib/src/scheduler/queue/undo.rs @@ -290,4 +290,48 @@ mod test { Ok(()) } + + #[test] + fn learning_cards_become_due_after_counts_cached() -> Result<()> { + use crate::scheduler::queue::learning::LearningQueueEntry; + + let mut col = Collection::new(); + if col.timing_today()?.near_cutoff() { + return Ok(()); + } + + // Add a note with learning cards + add_note(&mut col, true)?; + + // Answer to put a card into learning state + col.answer_again(); + assert_eq!(col.counts(), [1, 1, 0]); + + // Get the current queues to cache the counts + let queues = col.get_queues()?; + let old_cutoff = queues.current_learning_cutoff; + + // Manually add a learning card that would be due now but wasn't + // when the cutoff was set (simulating time passing) + let now = crate::timestamp::TimestampSecs::now(); + let new_entry = LearningQueueEntry { + due: now, // due right now + id: CardId(999), // fake ID + mtime: now, + }; + + // Insert the entry directly into the queue to simulate the bug scenario + let queues = col.state.card_queues.as_mut().unwrap(); + queues.intraday_learning.push_back(new_entry); + + // The old logic would not detect this newly due card because + // counts() only checked all_zero(), but our fix should detect it + let _updated_counts = queues.counts(); + + // The important thing is that update_learning_cutoff_and_count was called, + // which our fix should trigger. This updates the cutoff to the current time. + assert!(queues.current_learning_cutoff >= old_cutoff); + + Ok(()) + } }